Skip to content
Oliver Herrmann edited this page Sep 5, 2017 · 32 revisions

Differently from other great projects out there, RAW does not come with a strict framework to write charts. In RAW, charts can be written as you prefer. Instead, what RAW requires is some specification on how to build the necessary interface to let users transform their data and control some visual aspects of the chart.

RAW comes with APIs to create and edit charts. Please, refer to the API Reference for a complete description of all the methods available. Basically, there are two main objects in RAW: Models and Charts. Models create the data structure needed by charts, while Charts contain the code to draw the visualization. As a best practice and in order to easily modify them, both the model and the chart should be written within the same script file. Let's see how, through a simple concrete example.

Getting started

If you have not done this yet, please clone the RAW repository on your machine and follow the instructions to install dependencies and run it. You can now work on your local version and test new charts. If you would like to see your chart in the official release of RAW, please contact us or send us a pull request!

First of all, let's create a new script file for our chart (e.g. chart.js) within the charts/ folder and include it in the index.html:

<script src="charts/chart.js"></script>

While it is not strictly necessary, it is always better to wrap the whole chart code within a self-executing function, in order to isolate the scope and do not worry about other scripts' variable names.

(function(){
	// your code here...
})();

Creating the Model

The first step to add a new chart in RAW is defining which model the chart should use for the data. This is due to the fact that RAW works with tabular data, while many D3 layouts requires specific data structures (e.g. hierarchies for Bubble Charts or nodes-links for Force-directed Graphs). It becomes necessary then to transform the data records into the appropriate structure our chart works with. In this way users will be able to specify which columns they want to use - and how they want to use them - to construct the appropriate data structure.

Let's use a concrete example. We want to create a simple scatter plot chart to display two numeric dimensions in the dataset using Cartesian coordinates. Since each point in the scatter plot is defined by two coordinates, our chart will expect an array of objects containing an x and an y value, for each record in our dataset. Something like this:

[
	{ x: 0.1234, y: 56789 },
	{ x: 1.4321, y: 76895 },
	...
]

Defining Model Dimensions

We need to define a new model that allows the creation of the points array, starting from the data set. First of all, we need to construct the model:

var model = raw.model();

Then, we need to define the dimensions we want to expose to the user for the creation of the structure. For each dimension we define, RAW will create a GUI component to let the user associate one or more columns to that dimension.

As we have seen, for our scatter plot points, we need to define an x and y numeric dimension. Let's start with the x and create the dimension:

var x = model.dimension();

Adding a title is necessary to let the users know what the dimension is for:

x.title('X Axis');

Since the scatterplot works with numeric dimensions, we need to tell the users to associate this dimension only to numeric columns in their data. We do that by specifying its types:

x.types(Number);

Please, note that types are indicated using the native JavaScript class. Currently, the following data types are available: Number, String, Date. See the API Reference for more information.

However, since models allow function chaining, we could write the previous chunks of code in this way too:

var x = model.dimension()
   .title("X Axis")
   .types(Number);

Defining the y dimension is pretty similar:

var y = model.dimension()
    .title("Y Axis")
    .types(Number);

Map Function

Now that we have defined the dimensions of our model, we need to define the actual function to transform the data into the objects we want. We define this transformation within model's map function:

model.map(function(data) {
	return data.map(function(d) {
		return {
			x : +x(d),
			y : +y(d)
		}
	})
})

The function we define for model.map will receive the whole data set and its returning value will be passed directly to the chart. As you can see, in our case this function is pretty simple: for each object in the data, it creates a new object with the two properties x and y we need for our scatterplot. The values of those properties will depend on the columns the user associated to those dimensions (throught the GUI).

Dimensions work as accessors, when we pass them an object they return the value (of the column they are associated) for that object.

Here the all code for the model:

var model = raw.model();

var x = model.dimension() 
	.title('X Axis')
	.types(Number)

var y = model.dimension() 
	.title('Y Axis')
	.types(Number)

model.map(function (data){
	return data.map(function (d){
		return {
			x : +x(d),
			y : +y(d)
		}
	})
})

The Chart

Once we have defined the Model, we can move on to the Chart. The first step is to construct a new chart:

var chart = raw.chart();

Then, we need to specify the Model we want this chart to work with. In this way, charts do not have to worry about data transformation, but RAW will pass to them the proper structure resulting from the model:

chart.model(model);

We can specify both a title and a description, in order to give the user information about what the chart is and does:

chart.title("Simple scatter plot")
    .description("A simple scatter plot for learning purposes")

The next step is to define some options we want the users to control through the user interface.

Defining Chart Options, or Letting Users Customize the Visualization

Similarly to the dimensions in the model, options define those variables that we want to expose to the users to allow them to customize the visualization. But instead of controlling data transformation, chart options control visual properties of the chart (e.g. width, height, radius, paddings, colors, ...). Any time we want to let the user control one of these aspect we can create an option. Similarly to dimensions, RAW will create an appropriate GUI element to let the user control that property.

In our case, we want the user to control the size of the chart (i.e. width and height) as well as its margins. Let's start with the width..

First of all we need to construct the options:

var width = chart.number();

Since the chart width is a numeric value, we need to control it through a number option. Currently, RAW allows the creation of the following option types: numbers, checkbox, lists and colors. Each of these options provides its own methods, please refer to the API Reference for more details about them.

We provide a title to indicate what this option is about:

width.title('Width')

And an initial value:

width.defaultValue(900)

As before, we can write the same code in this way:

var width = chart.number()
    .title('Width')
    .defaultValue(900)

Similarly, we create an option for both the height

var height = chart.number()
    .title('Height')
    .defaultValue(600)

and the margin

var margin = chart.number()
    .title('Margin')
    .defaultValue(10)

Drawing Function

Finally, we need to define the chart, using the draw function. This function is called passing two arguments: the selection and the data. The selection represents the svg element (as a common D3 selection) where the visualization will appear in RAW, while the data is the data structure resulting from running the model on the original records:

chart.draw(function(selection,data) {
	// selection is the svg element
	// data is the data structure resulting from the application of the model
	// generate chart here...
})

Important! This function will be called any time the users change an option value. The svg will be cleared everytime before calling this function. In this way, D3's enter-update-exit pattern does not make too much sense within RAW's charts, since the selection is always empty when passed to the draw function. Since RAW is meant to be a tool for the production of non-interactive visualizations, to be elaborated using vector-graphics tools, this should not be perceived as a limitation, but, at the contrary, as a way to simplify charts' drawing code.

This also implies that charts in RAW must use only SVG elements. Moreover, in order to include all the styles within the SVG code and allow the users to export exactly what they are looking at in RAW, External CSS styles cannot be used. Every style or attribute declaration has to be explicitly defined in the draw function (i.e. through .style or .attr D3's operators).

The first thing we do within our drawing function is to set the size of the svg using the chart options - width and height - we have defined above:

selection
	.attr("width", width())
	.attr("height", height())

As you can see, to get the value of each option we simply call it. The value will correspond to the one set by the user thorugh the GUI element connected to it. The type of value can vary according to the type of option. In this case, since the two options are numeric ones, the returned values will be numbers.

Our scatter plot will need two linear scales, to properly scale the x and y values of the points to our svg size. To calculate the domain of the scales we will use the d3.max function on the array of points, specifying which key to use (let's assuming all the values will be positive and thus having 0 as the minimum value). Since we want to allow the user define a margin for our visualization, we will include that into the scales:

var xScale = d3.scale.linear()
	.domain([0, d3.max(data, function (d){ return d.x; })])
	.range([margin(), width()-margin()]);
	
var yScale = d3.scale.linear()
	.domain([0, d3.max(data, function (d){ return d.y; })])
	.range([height()-margin(), margin()]);

Now we can draw the points, by using simple svg circle elements:

selection.selectAll("circle")
	.data(data)
	.enter().append("circle")
	.attr("cx", function(d) { return xScale(d.x); })
	.attr("cy", function(d) { return yScale(d.y); })
	.attr("r", 5)

Summing up, our draw function will look like this:

chart.draw(function (selection, data){

	// svg size
	selection
		.attr("width", width())
		.attr("height", height())

	// x and y scale
	var xScale = d3.scale.linear()
		.domain([0, d3.max(data, function (d){ return d.x; })])
		.range([margin(), width()-margin()]);
			
	var yScale = d3.scale.linear()
		.domain([0, d3.max(data, function (d){ return d.y; })])
		.range([height()-margin(), margin()]);

	// let's plot the dots
	selection.selectAll("circle")
		.data(data)
		.enter().append("circle")
		.attr("cx", function(d) { return xScale(d.x); })
		.attr("cy", function(d) { return yScale(d.y); })
		.attr("r", 5)
})

You can find the code for this chart here.