Extension:Graph/Interactive Graph Tutorial

In this tutorial we will create an interactive graph that will display historical fertility rates per country, with a slider to pick the year, and a map to show rate distribution around the world. See also complete Vega documentation. The definition for this graph is here.

Drawing Shapes
We start with drawing a few elements (marks). The graph code is surrounded by the  tag, even though the graph is not yet interactive.

Hint: Use "edit source" to copy the graph spec to the Vega Editor for experimentation.

{ "version": 2, "width": 300, "height": 80, "padding": 12, "background": "#edf1f7", "marks": [ {     "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 300}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "x": {"value": 200}, "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": {"fill": {"value": "#fff"}}, "hover": {"fill": {"value": "#f00"}} }   }  ] }

IsDragging signal
To make our handle object dragable, we first need to figure out if it was clicked or not. For that, lets add a signal that becomes true when an object is clicked (isDragging), and a text mark to show (debug) the result:

{ "version": 2, "width": 300, "height": 80, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   }  ],  "marks": [ {     "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 300}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "x": {"value": 200}, "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": {"fill": {"value": "#fff"}}, "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "debugIsDragging", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 0}, "fill": {"value": "black"} },       "update": {"text": {"signal": "isDragging"}} }   }  ] }

Handle Position signal
Now that we know when the object is being dragged, we add a mousemove signal that only changes its value when isDragging signal is true. We also attach the new signal to the handle's "x" coordinate via "update" section:

{ "version": 2, "width": 300, "height": 80, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "handlePosition", "init": 200, "streams": [{"type": "mousemove[isDragging]","expr": "eventX"}] } ],  "marks": [ {     "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 300}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"signal": "handlePosition"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "debugIsDragging", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 0}, "fill": {"value": "black"} },       "update": {"text": {"signal": "isDragging"}} }   },    {      "name": "debugHandlePosition", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 14}, "fill": {"value": "black"} },       "update": {"text": {"signal": "handlePosition"}} }   }  ] }

Scaling Handle Position signal
Having pixel position of the handler is not very good - we would much rather have a position that is meaningful to our graph, e.g. a year. Vega scales provide a useful mechanism for converting between our data (e.g. years) and the screen coordinates, and back (invert). In this step, we add "yearsScale" linear scale for values 1960..2013, mapping it to the whole width of the graph (excluding padding). We also add a new scaledHandlePosition signal that translates from the mouse X position to the meaningful value in years. { "version": 2, "width": 300, "height": 80, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "handlePosition", "init": 200, "streams": [{"type": "mousemove[isDragging]","expr": "eventX"}] },   {      "name": "scaledHandlePosition", "expr": "handlePosition", "scale": {"name": "yearsScale","invert": true} } ],  "scales": [ {     "name": "yearsScale", "type": "linear", "zero": false, "domain": [1960, 2013], "range": "width" } ],  "marks": [ {     "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 300}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"signal": "handlePosition"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "debugIsDragging", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 0}, "fill": {"value": "black"} },       "update": {"text": {"signal": "isDragging"}} }   },    {      "name": "debugHandlePosition", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 14}, "fill": {"value": "black"} },       "update": {"text": {"signal": "handlePosition"}} }   },    {      "name": "debugScaledHandlePosition", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 28}, "fill": {"value": "black"} },       "update": {"text": {"signal": "scaledHandlePosition"}} }   }  ] }

Year value cleanup
As you might have noticed, the handle could be moved beyond the length of our graph, producing incorrect results. Also, the scaled value is not an integer that we expect of the year value. To fix this, we can introduce one more post-processing signal called "currentYear" to convert it to an integer and limit it to the needed range. We also initialize it to a reasonable value, and we tie both the "yearLabel" and the position of the handle bar back to this value. Note that the handle's position needs to be in screen coordinates, so we have to use the "yearsScale" to convert the value back:

{ "version": 2, "width": 300, "height": 80, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "handlePosition", "streams": [{"type": "mousemove[isDragging]","expr": "eventX"}] },   {      "name": "scaledHandlePosition", "expr": "handlePosition", "scale": {"name": "yearsScale","invert": true} },   {      "name": "currentYear", "init": 2000, "expr": "clamp(parseInt(scaledHandlePosition),1960,2013)" } ],  "scales": [ {     "name": "yearsScale", "type": "linear", "zero": false, "domain": [1960, 2013], "range": "width" } ],  "marks": [ {     "name": "yearLabel", "type": "text", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 25}, "fontSize": {"value": 32}, "fontWeight": {"value": "bold"}, "fill": {"value": "steelblue"} },       "update": {"text": {"signal": "currentYear"}} }   },    {      "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 300}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"scale": "yearsScale","signal": "currentYear"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "debugIsDragging", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 0}, "fill": {"value": "black"} },       "update": {"text": {"signal": "isDragging"}} }   },    {      "name": "debugHandlePosition", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 14}, "fill": {"value": "black"} },       "update": {"text": {"signal": "handlePosition"}} }   },    {      "name": "debugScaledHandlePosition", "type": "text", "properties": { "enter": { "x": {"value": 250}, "y": {"value": 28}, "fill": {"value": "black"} },       "update": {"text": {"signal": "scaledHandlePosition"}} }   }  ] }

Final cleanup
Now we can remove all the debugging marks. We also don't need separate handlePosition and scaledHandlePosition signals because scaling can happen in the same step:

{ "version": 2, "width": 300, "height": 80, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "scaledHandlePosition", "streams": [ {         "type": "mousemove[isDragging]", "expr": "eventX", "scale": {"name": "yearsScale","invert": true} }     ]    },    {      "name": "currentYear", "init": 2000, "expr": "clamp(parseInt(scaledHandlePosition),1960,2013)" } ],  "scales": [ {     "name": "yearsScale", "type": "linear", "zero": false, "domain": [1960,2013], "range": "width" } ],  "marks": [ {     "name": "yearLabel", "type": "text", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 25}, "fontSize": {"value": 32}, "fontWeight": {"value": "bold"}, "fill": {"value": "steelblue"} },       "update": {"text": {"signal": "currentYear"}} }   },    {      "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 300}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"scale": "yearsScale","signal": "currentYear"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   }  ] }

Drawing a map with some useful data
Now that the slider is working, we can add a map and mix it with the highlighting data. The map is stored as a raw TopoJSON text in wiki markup here. Each country is tagged with a ISO-3 country code. For this example, we will use fertility data that is stored as a CSV text in wiki markup here, copied from WorldBank (license). The CSV contains "country", "id" columns, as well as years 1960..2013, each as a separate column.

 { "version": 2, "width": 500, "height": 260, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "scaledHandlePosition", "streams": [ {         "type": "mousemove[isDragging]", "expr": "eventX", "scale": {"name": "yearsScale","invert": true} }     ]    },    {      "name": "currentYear", "init": 2000, "expr": "clamp(parseInt(scaledHandlePosition),1960,2013)" } ],  "scales": [ {     "name": "yearsScale", "type": "linear", "zero": false, "domain": [1960,2013], "range": "width" },   {      "name": "color", "type": "linear", "domain": {"data": "highlights","field": "v"}, "zero": true, "range": ["#ffff65","#cb0000"] } ],  "data": [ {     "name": "highlights", "url": "https://www.mediawiki.org/w/index.php?title=Extension:Graph/Demo/RawData:FertilityByCountryHistoric-csv&action=raw", "format": {"type": "csv"}, "transform": [ {         "type": "formula", "field": "v", "expr": "parseFloat(datum[''+currentYear])" }     ]    },    {      "name": "countries", "url": "https://www.mediawiki.org/w/index.php?title=Extension:Graph/Demo/RawData:WorldMap-iso3-json&action=raw", "format": {"type": "topojson","feature": "countries"}, "transform": [ {         "type": "geopath", "value": "data", "scale": 80, "center": [-180,125], "translate": [0,0], "projection": "equirectangular" },       {          "type": "lookup", "keys": ["id"], "on": "highlights", "onKey": "id", "as": ["zipped"], "default": {"v": null} }     ]    }  ],  "marks": [ {     "name": "yearLabel", "type": "text", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 25}, "fontSize": {"value": 32}, "fontWeight": {"value": "bold"}, "fill": {"value": "steelblue"} },       "update": {"text": {"signal": "currentYear"}} }   },    {      "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 500}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"scale": "yearsScale","signal": "currentYear"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "map", "type": "path", "from": {"data": "countries"}, "properties": { "enter": {"path": {"field": "layout_path"}}, "update": {"fill": {"scale": "color","field": "zipped.v"}}, "hover": {"fill": {"value": "#989898"}} }   }  ] }

Adding Legend
Graph will be more informative if the user sees how colors correspond to numbers. Unfortunatelly Legend placement is a bit broken at the moment and will need to be fixed in Vega, but it tends to work in non-map based graphs.

 { "version": 2, "width": 500, "height": 260, "padding": 12, "background": "#edf1f7", "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "scaledHandlePosition", "streams": [ {         "type": "mousemove[isDragging]", "expr": "eventX", "scale": {"name": "yearsScale","invert": true} }     ]    },    {      "name": "currentYear", "init": 2000, "expr": "clamp(parseInt(scaledHandlePosition),1960,2013)" } ],  "scales": [ {     "name": "yearsScale", "type": "linear", "zero": false, "domain": [1960,2013], "range": "width" },   {      "name": "color", "type": "linear", "domain": {"data": "highlights","field": "v"}, "zero": true, "range": ["#ffff65","#cb0000"] } ],  "data": [ {     "name": "highlights", "url": "https://www.mediawiki.org/w/index.php?title=Extension:Graph/Demo/RawData:FertilityByCountryHistoric-csv&action=raw", "format": {"type": "csv"}, "transform": [ {         "type": "formula", "field": "v", "expr": "parseFloat(datum[''+currentYear])" }     ]    },    {      "name": "countries", "url": "https://www.mediawiki.org/w/index.php?title=Extension:Graph/Demo/RawData:WorldMap-iso3-json&action=raw", "format": {"type": "topojson","feature": "countries"}, "transform": [ {         "type": "geopath", "value": "data", "scale": 80, "center": [-180,125], "translate": [0,0], "projection": "equirectangular" },       {          "type": "lookup", "keys": ["id"], "on": "highlights", "onKey": "id", "as": ["zipped"], "default": {"v": null} }     ]    }  ],  "legends": [ {     "fill": "color", "title": "Fertility", "offset":-300, "properties": { "gradient": { "stroke": {"value": "transparent"} },       "title": { "fontSize": {"value": 14} },     }    }  ],  "marks": [ {     "name": "yearLabel", "type": "text", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 25}, "fontSize": {"value": 32}, "fontWeight": {"value": "bold"}, "fill": {"value": "steelblue"} },       "update": {"text": {"signal": "currentYear"}} }   },    {      "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 500}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"scale": "yearsScale","signal": "currentYear"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "map", "type": "path", "from": {"data": "countries"}, "properties": { "enter": {"path": {"field": "layout_path"}}, "update": {"fill": {"scale": "color","field": "zipped.v"}}, "hover": {"fill": {"value": "#989898"}} }   }  ] }

Adding Some Statistics
The map mouseover event will cause the country and the associated rate show up in the right corner.

 { "version": 2, "width": 500, "height": 260, "padding": 12, "background": "#edf1f7", "predicates": [ {     "name": "isNotNull", "type": "!=", "operands": [{"value": null}, {"arg": "id"}] } ],  "signals": [ {     "name": "isDragging", "init": false, "streams": [ {"type": "@handle:mousedown","expr": "true"}, {"type": "mouseup","expr": "false"} ]   },    {      "name": "scaledHandlePosition", "streams": [ {         "type": "mousemove[isDragging]", "expr": "eventX", "scale": {"name": "yearsScale","invert": true} }     ]    },    {      "name": "currentYear", "init": 2000, "expr": "clamp(parseInt(scaledHandlePosition),1960,2013)" },   {      "name": "tooltipSignal", "init": {"expr": "{x: 0, y: 0, datum: {zipped:{} } }"}, "streams": [ {           "type": "@map:mouseover", "expr": "{x: eventX, y: eventY, datum: eventItem.datum}" },       {            "type": "@map:mouseout", "expr": "{x: 0, y: 0, datum: {zipped:{} } }" }     ]     }  ],  "data": [ {     "name": "highlights", "url": "https://www.mediawiki.org/w/index.php?title=Extension:Graph/Demo/RawData:FertilityByCountryHistoric-csv&action=raw", "format": {"type": "csv"}, "transform": [ {         "type": "formula", "field": "v", "expr": "parseFloat(datum[''+currentYear])" }     ]    },    {      "name": "countries", "url": "https://www.mediawiki.org/w/index.php?title=Extension:Graph/Demo/RawData:WorldMap-iso3-json&action=raw", "format": {"type": "topojson","feature": "countries"}, "transform": [ {         "type": "geopath", "value": "data", "scale": 80, "center": [-180,125], "translate": [0,0], "projection": "equirectangular" },       {          "type": "lookup", "keys": ["id"], "on": "highlights", "onKey": "id", "as": ["zipped"], "default": {"v": null, "country":"No data"} }     ]    }  ],  "scales": [ {     "name": "yearsScale", "type": "linear", "zero": false, "domain": [1960,2013], "range": "width" },   {      "name": "color", "type": "linear", "domain": {"data": "countries","field": "zipped.v"}, "domainMin": 1, "zero": false, "range": ["#FFEDBC", "#f83600"] } ],  "marks": [ {     "name": "yearLabel", "type": "text", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 25}, "fontSize": {"value": 32}, "fontWeight": {"value": "bold"}, "fill": {"value": "steelblue"} },       "update": {"text": {"signal": "currentYear"}} }   },    {      "name": "scrollLine", "type": "rule", "properties": { "enter": { "x": {"value": 0}, "y": {"value": 40}, "x2": {"value": 500}, "stroke": {"value": "#000"}, "strokeWidth": {"value": 2} }     }    },    {      "name": "handle", "type": "path", "properties": { "enter": { "y": {"value": 40}, "path": {"value": "m-5.5,-10l0,20l11.5,-10l-11.5,-10z"}, "stroke": {"value": "#880"}, "strokeWidth": {"value": 2.5} },       "update": { "x": {"scale": "yearsScale","signal": "currentYear"}, "fill": {"value": "#fff"} },       "hover": {"fill": {"value": "#f00"}} }   },    {      "name": "map", "type": "path", "from": {"data": "countries"}, "properties": { "enter": {"path": {"field": "layout_path"}}, "update": { "fill":{ "rule": [ {               "predicate": { "name": "isNotNull", "id": {"field": "zipped.v"} },               "scale": "color", "field": "zipped.v"             }, {"value": "grey"} ]         }        },        "hover": {"fill": {"value": "#989898"}} }   },    {    "type": "text", "properties": { "enter": { "x": {"value": 500}, "y": {"value": 10}, "align": {"value": "right"}, "fontSize": {"value": 17}, "fill": {"value": "black"} },     "update": { "text": {"template": "\u007b{tooltipSignal.datum.zipped.country}} \u007b{tooltipSignal.datum.zipped.v}}"} }   }}  ],  "legends": [ {     "fill": "color", "title": "Fertility", "offset":-300, "properties": { "gradient": { "stroke": {"value": "transparent"} },       "title": { "fontSize": {"value": 14} }     }    }  ] }