• Jump To … +
    butterfly_example.ts dark_example.ts flamechart_example.ts icicle_sunburst_example.ts legend_example.ts pie_example.ts plot_area_example.ts plot_zoom_example.ts region_example.ts sankey_example.ts scatterplot_example.ts stack_example.ts swimlanes_example.ts sync_example.ts table_searchable_example.ts table_selectable_example.ts table_sortable_example.ts timeline_example.ts worldbank_example.ts
  • ¶

    C3 Crossfilter Dataset

    An example the includes many plots and tables synchronized with a common dataset using [**Crossfilter **][Crossfilter]. This example also includes more demonstration on how to style and extend custom charts. [Crossfilter]: http://square.github.io/crossfilter/ “Crossfilter Library”

    Crossfilter is a great library for managing large multivariate datasets and filtering on multiple dimensions.

  • ¶

    ###########################################################################

    Load the Data

  • ¶

    Three data models for this example to visualize.

    • gdp_data - array of objects with country_name, country_code, year, and other properties from World Bank indicators based on the indicator_mapping.
    interface GDPData {
        country_name: string;
        country_code: string;
        year: number;
        population: number;
        urban_population: number;
        gdp: number;
        gdp_per_capita: number;
        land_area: number;
        agriculture: number;
        manufacturing: number;
        services: number;
    }
  • ¶
    • counties - map of country codes to objects with name, code, and region_id.
    interface Country {
        name: string;
        code: string;
        region_id: number;
    }
  • ¶
    • regions - map of region ids to objects with name, id, and parent_id.
    interface Region {
        name: string;
        id: number;
        parent_id: number;
    }
  • ¶

    To keep things simple this example doesn’t use a proper MVC architecture, so we’ll just use some simple global objects:

    var gdp_data: GDPData[] = [];
    var countries: { [code: string]: Country } = {};
    var regions: { [id: number]: Region } = {};
  • ¶

    The following function will download the sample datasets about countries, GDP metrics, etc from CSV files. For purposes of understanding the C3 visualization you can skip this section. Most of it is about transforming the data from the format that the World Bank provides to a format best suited to working with the Crossfilter library. We could have done this transformation ahead of time and just used that modified CSV, but I wanted to work with the CSV in the original format to be able to update the data or get new metrics from the World Bank.

    function download_data() {
        var indicator_mapping = {
            'SP.POP.TOTL': 'population',
            'EN.URB.MCTY.TL.ZS': 'urban_population',
            'NY.GDP.MKTP.CD': 'gdp',
            'NY.GDP.PCAP.CD': 'gdp_per_capita',
            'AG.LND.TOTL.K2': 'land_area',
            'NV.AGR.TOTL.CD': 'agriculture',
            'NV.IND.MANF.CD': 'manufacturing',
            'NV.SRV.TETC.CD': 'services',
        };
  • ¶

    This function returns a promise to easily manage multiple CSV dependencies asynchronously. The user provides a callback to the promise that will exectute when the promise is resolved. Promise.all() will create a promise with multiple dependencies, one for each of the three CSV files to download and process.

        return Promise.all([
            new Promise((resolve) => {
  • ¶

    Load World Bank Data CSV

  • ¶

    Use D3 to download the GDP and other metrics from the World Bank CSV file.

                d3.csv('data/gdp.csv')
  • ¶

    The row callback is used to structure what the rows look like based on the CSV data. Notice that the numerical values use a + to parse the string to a numerical value.

                    .row((row) => ({
                        country_name: row['Country Name'],
                        country_code: row['Country Code'],
                        indicator: row['Indicator Code'],
                        value: +row['Value'],
                        year: +row['Year'],
                    }))
  • ¶

    The get callback is used to process the data once it is loaded. The World Bank provides the data in a format with a single value per row. This code translates that to a format with a row for each country and year combination and a column for each additional metric provided.

                    .get((error, rows) => {
                        var tmp_map = {};
                        for (let row of rows) {
                            if (!row.country_code) continue;
                            let data_row;
                            if (!(data_row = tmp_map[row.country_code + row.year]))
                                data_row = tmp_map[row.country_code + row.year] = {
                                    country_name: row.country_name,
                                    country_code: row.country_code,
                                    year: row.year,
                                };
                            data_row[indicator_mapping[row.indicator]] = row.value;
                        }
                        gdp_data = [];
                        for (let k in tmp_map) gdp_data.push(tmp_map[k]);
  • ¶

    Convert the GDP metrics to billions of dollars for convenience and ensure GDP isn’t 0 if the data is missing to avoid divide by zero problems.

                        for (let record of gdp_data) {
                            for (let metric of ['gdp', 'agriculture', 'manufacturing', 'services'])
                                record[metric] /= 1000000000;
                            if (!record.gdp) record.gdp = 1;
                        }
  • ¶

    Resolve this promise to indicate that this dependency has been met.

                        resolve();
                    });
            }),
  • ¶

    Load the country data CSV

            new Promise((resolve) => {
                d3.csv('data/countries.csv')
                    .row((row) => ({
                        name: row['name'],
                        code: row['world_bank_code'],
  • ¶

    Countries include a link to their region.

                        region_id: +row['region-code'],
                    }))
  • ¶

    When the raw CSV has been loaded convert it to an associative map based on country ID. Normally I found it more efficient to manage this data as an array where the index into the array cooresponds to the ID, but just keeping the example simple here.

                    .get((error, rows) => {
                        rows.filter((row) => !!row.code).forEach((row) => {
                            countries[row.code] = row;
                        });
                        resolve();
                    });
            }),
  • ¶

    Load the region info CSV

    Convert it to an associative object, and resolve its promise when done.

            new Promise((resolve) => {
                d3.csv('data/regions.csv')
                    .row((row) => ({
                        name: row['name'],
                        id: +row['id'],
                        parent_id: +row['parent_id'],
                    }))
                    .get((error, rows) => {
                        for (let row of rows)
                            regions[row.id] = row;
                        resolve();
                    });
            }),
  • ¶

    After all the CSV data has been loaded and processed, remove the wait notification and check the dataset if any countries referenced by the World Bank data are missing from the countries table.

        ]).then(function () {
            d3.selectAll('#waiting').remove();
            d3.selectAll('main').style('display', '');
            for (let record of gdp_data.filter((record) => !countries[record.country_code]))
                throw Error("Missing country: " + record.country_code + " - " + record.country_name);
        });
    }
  • ¶

    ###########################################################################

    Create and Render the Charts

  • ¶

    This function will create the various charts and tables for this example.

    function render() {
  • ¶

    Prepare the data

    Output the data set to the console if you want to take a look.

        console.log("Data:", gdp_data);
        console.log("Regions:", regions);
        console.log("Countries:", countries);
  • ¶

    Setup the core metrics dataset with Crossfilter

        var xf = crossfilter(gdp_data);
  • ¶

    groupAll() allows us to get a single value for the entire dataset. In this case we do a reduction based on the total population.

        var total_population = xf.groupAll<number>().reduceSum((d) => d.population);
        var total_gdp = xf.groupAll<number>().reduceSum((d) => d.gdp);
  • ¶

    Prepare the Crossfilter dimensions we can filter the data on. With this we can filter based on country, region, year, urbanization, etc. Notice the region dimension is added even though the dataset doesn’t link directly to a region. The agriculture, services, and manufacturing dimensions are also created based on their percentage of GDP instead of an absolute value.

        var country_dim = xf.dimension((d) => d.country_code);
        var region_dim = xf.dimension((d) => countries[d.country_code].region_id);
        var year_dim = xf.dimension((d) => d.year);
        var urban_dim = xf.dimension((d) => d.urban_population);
        var agriculture_dim = xf.dimension((d) => Math.min(d.agriculture / d.gdp * 100, 100));
        var services_dim = xf.dimension((d) => Math.min(d.services / d.gdp * 100, 100));
        var manufacturing_dim = xf.dimension((d) => Math.min(d.manufacturing / d.gdp * 100, 100));
  • ¶

    Store an array of all the visualizations in this example.Create redraw() and restyle() functions to update all of the charts in this example when the dataset is updated via Crossfilter. C3 doesn’t try to automatically update or resize charts because that can be expensive and so it lets the user indicate when that needs to be done.

        var charts: c3.Base[] = [];
        function redraw() {
            for (let chart of charts)
                chart.redraw();
        }
        function restyle() {
            for (let chart of charts)
                chart.restyle();
        }
  • ¶

    These variables will track the country or region the user’s mouse is currently hovering over.

        var hover_country;
        var hover_region;
  • ¶

    ###########################################################################

    GDP Timeline Chart

  • ¶

    Crossfiter dimensions are great for filtering the dataset. But, we’d also like to visualize the data based on those dimensions. This first chart will be a timeline, so we’ll use the year_dim dimension. The dimension has a function called group() which will generate a grouping based on the data. We use reduceSum() to reduce each entry in the grouping by summing a value specified via a callback. In this way we can get an arrangement of the data that is grouped by year, but provides values such as GDP, agriculture GDP, etc. The all() method returns us an array of this grouping that is easy to use. Each entry in the array contains a key based on the dimension (the year in this case) and a value based on the grouping’s reduction.

        var gdp_by_year_data = year_dim.group().reduceSum((d) => d.gdp).all();
        var agriculture_by_year_data = year_dim.group().reduceSum((d) => d.agriculture).all();
        var manufacturing_by_year_data = year_dim.group().reduceSum((d) => d.manufacturing).all();
        var services_by_year_data = year_dim.group().reduceSum((d) => d.services).all();
  • ¶

    Output the gdp per year data to the console so you can take a look at what it looks like.

        console.log("gdp_by_year_data", gdp_by_year_data);
  • ¶

    Create a new c3.Plot for the timeline.

        var timeline;
        charts.push(timeline = new c3.Plot.Zoomable<CrossFilter.Grouping<number, number>>({
            anchor: '#worldbank_timeline',
            height: 300,
  • ¶

    Setup the scales. Time will go from the year 1970-2012. The default chart vertical scale will go from 0-100 for percentages.

            h: d3.scale.linear().domain([1970, 2012]),
            v: d3.scale.linear().domain([0, 100]),
  • ¶

    The x function uses the key directly, which represents the year from the year dimension.

            x: (d) => d.key,
  • ¶

    The default y function generates its value as a percentage of the gdp for that year. There are better ways to do this, but I wanted to demonstrate the flexibility the callbacks provide for visualizing based on the data.

            y: (d) => d.value > 1 ? d.value / gdp_by_year_data[d.key - 1970].value * 100 : 0,
  • ¶

    Set chart as zoomable and limit the maximum amount the user is allowed to zoom in.

            zoomable: 'h',
            zoom_extent: 16,
  • ¶

    Set margins on all sides of the chart to allow extra room for labels to fit.

            margins: 8,
  • ¶

    Create axes for this plot.

            axes: [
                new c3.Axis.X({
                    label: "Year",
                    grid: true,
  • ¶

    Use a D3 formatter here to avoid commas showing up in the year

                    tick_label: d3.format('f'),
                }),
                new c3.Axis.Y({
                    label: "% of GDP",
                    grid: true,
  • ¶

    Use our own formatter to add a “%” symbol after the number

                    tick_label: (n) => n + "%",
                }),
  • ¶

    Create a third axis for this plot on the right side.This axis will indicate the scale for the GDP layer which is expressed in dollars instead of percentage. It also shows that we can use a different scale here to show one way we can represent different units, trillions of dollars instead of billions.

                new c3.Axis.Y({
                    label: "GDP in USD",
                    orient: 'right',
                    scale: d3.scale.linear().domain([0, d3.max(gdp_by_year_data, (d) => d.value / 1000)]),
                    tick_label: (n) => "$" + n.toLocaleString() + "t",
                    axis_size: 75,
                }),
            ],
  • ¶

    Create the layers for this timeline.

            layers: [
  • ¶

    The first layer is an area graph for the world GDP value. This doesn’t use the chart’s default vertical scale and instead uses its own scale based on the GDP dollars. The top of the chart is set to the maximum GDP value in our dataset.

                new c3.Plot.Layer.Area<CrossFilter.Grouping<number, number>>({
                    options: {
                        title: "GDP",
                        class: 'gdp',
                    },
                    data: gdp_by_year_data,
                    v: d3.scale.linear().domain([0, d3.max(gdp_by_year_data, (d) => d.value)]),
                    y: (d) => d.value,
                    interpolate: 'cardinal',
                }),
  • ¶

    Notice how the class for each layer is set here. This allows the example to determine how these individual layers appear with an efficient stylesheet and avoid DOM manipulation. It also allows for dynamically updating the styles, such as when hovering over the cooresponding legend item.

                new c3.Plot.Layer.Line<CrossFilter.Grouping<number, number>>({
                    options: {
                        title: "% Services",
                        class: 'services',
                    },
                    data: services_by_year_data,
                }),
                new c3.Plot.Layer.Line<CrossFilter.Grouping<number, number>>({
                    options: {
                        title: "% Manufacturing",
                        class: 'manufacturing',
                    },
                    data: manufacturing_by_year_data,
                }),
                new c3.Plot.Layer.Line<CrossFilter.Grouping<number, number>>({
                    options: {
                        title: "% Agriculture",
                        class: 'agriculture',
                    },
                    data: agriculture_by_year_data,
                }),
  • ¶

    This layer represents a vertical line for the currently selected year.

                new c3.Plot.Layer.Line.Vertical<number>({
                    data: [2000],
                    draggable: true,
    
                    options: {
                        title: "Selected Year",
                        class: 'selected_year',
                    },
  • ¶

    Setup an event handler when the user drags the line to update the filtering to only show data for the selected year. Also update the current year and world population at the time and redraw all charts to reflect the updated dataset filtering.

                    handlers: {
                        'drag': function (year) {
                            this.data = [year];
                            year = Math.round(year);
                            year_dim.filter(year);
                            d3.selectAll('#sync_example_year').text(year);
                            d3.selectAll('#total_population').text(d3.format(',')(total_population.value()));
                            redraw();
                        },
  • ¶

    Filter the data based on the initial year selection

                        'render': function () {
                            this.handlers['drag'](this.data[0]);
                        },
                    },
                }),
            ],
        }));
  • ¶

    ###########################################################################

    Timeline Legend

  • ¶

    Create a C3 legend and link it with the timeline C3 plot we just created.

        charts.push(new c3.Legend.PlotLegend({
            anchor: '#worldbank_legend',
            plot: timeline,
  • ¶

    Setup event handlers when hovering over legend items to pulse the opacity across all charts on the page with the same class as this layer. This demonstrates a way that classes can be used to synchronize related data across the entire page and different visualizations.

            handlers: {
                'layer_mouseenter': (layer) => {
                    d3.selectAll('.' + layer.options.class).classed('legend_hover', true);
                },
                'layer_mouseleave': (layer) => {
                    d3.selectAll('.' + layer.options.class).classed('legend_hover', false);
                },
            },
        }));
  • ¶

    ###########################################################################

    Region Table

  • ¶

    Use Crossfilter to get an array of data just like above. Only, in this case, we are using the region_dim dimension instead of the year dimension. For each region, this grouping will reduce the value based on GDP.

        var gdp_by_region_data = region_dim.group().reduceSum((d) => d.gdp).all();
  • ¶

    Use this D3 utility function to colorize our regions.

        var region_color = d3.scale.category10<number>();
  • ¶

    Create a c3.Table for the regions table.

        var regions_table: c3.Table<CrossFilter.Grouping<number, number>>;
        var gdp_column: c3.Table.Column<CrossFilter.Grouping<number, number>>;
        charts.push(regions_table = new c3.Table<CrossFilter.Grouping<number, number>>({
            anchor: '#worldbank_region_table',
  • ¶

    Bind it to the data prepared above. The key‘s represent region_ids and the value`’s represent gdp.

            data: gdp_by_region_data,
  • ¶

    Create two columns for this table. One with the region name and another for the GDP value

            columns: [
                {
                    header: { text: "Region" },
                    cells: { text: (d) => regions[d.key].name },
                },
                gdp_column = {
                    header: { text: "GDP in $b" },
  • ¶

    Use a custom html formatter for the cell contents

                    cells: { html: (d) => d3.format(',')(Math.round(Math.abs(d.value))) },
  • ¶

    The value of this cell used for the bar visualization and for sorting.

                    value: (d) => Math.abs(d.value),
  • ¶

    In addition to the html content above also use a C3 bar visualization of the data This will render a bar graph inside the table cell whose width is based on the cell value. vis_options.styles is used to set the color for this bar.

                    vis: 'bar',
                    vis_options: {
                        styles: {
                            'background-color': (d) => region_color(d.key),
                        },
                    },
                },
            ],
  • ¶

    The initial sort.

            sort_column: gdp_column,
  • ¶

    Allow the user to select multiple items in the table

            selectable: 'multi',
  • ¶

    row_options is used to dynamically adjust rows. row_options.classes will assign CSS classes to rows. The class name can be anything you like. In this example we set the rows to have class hover if the row’s region is the same as the region the user is hovering over with their mouse or if they are hovering over a country in that region. The stylesheet then causes any table rows with the class hover to have a wheat-colored background.

            row_options: {
                classes: {
                    'hover': (d) => regions[d.key] === hover_region || (hover_country && d.key === hover_country.region_id),
                },
  • ¶

    row_options.events will set event handlers for rows in the table. Here we set handlers for when the mouse hovers over the row. When it does, we record the region the user is hovering over and call restyle() to restyle the charts. This way all of the charts in the example can highlight or somehow indicate this region of interest.

                events: {
                    mouseenter: (d) => {
                        hover_region = regions[d.key];
                        restyle();
                    },
                    mouseleave: (d) => {
                        hover_region = null;
                        restyle();
                    },
                },
            },
        }));
  • ¶

    As an alternative to declaratively setting handlers for the chart, you can imperatively call .on() to manage event handlers.

    If the user selects one or more rows in the table, then we will filter the data based on those regions using Crossfilter. redraw() is then used to update the charts based on the updated data.

        regions_table.on('select', (selections) => {
            if (selections && selections.length)
                region_dim.filter((key) => selections.map((d) => d.key).indexOf(key) >= 0);
            else
                region_dim.filterAll();
            redraw();
        });
  • ¶

    ###########################################################################

    Country Scatter Plot

  • ¶

    Now we’ll create a fun scatter plot of all of the countries. For this we want to use the country dimension. But, we want more than one piece of information about each country. There are multiple ways to do this, and for larger datasets I would use a different approach. However, I wanted to demonstrate here how to use custom reduction functions. Another possible use of custom reduction functions might be to compute an average instead of a sum. reduce() takes three callbacks. The third one is used to initially create the “values” for this grouping. The first and second are used to add and remove data from those values based on if that data item is filtered in or out. Crossfilter is surprisingly efficient about this.

        interface CountryData {
            gdp: number;
            gdp_per_capita: number;
            population: number;
            land_area: number;
        }
        var country_data = country_dim.group().reduce<CountryData>(
            function (p, v) {
                p.gdp += v.gdp;
                p.gdp_per_capita += v.gdp_per_capita;
                p.population += v.population;
                p.land_area += v.land_area;
                return p;
            },
            function (p, v) {
                p.gdp -= v.gdp;
                p.gdp_per_capita -= v.gdp_per_capita;
                p.population -= v.population;
                p.land_area -= v.land_area;
                return p;
            },
            function () { return { gdp: 0, gdp_per_capita: 0, population: 0, land_area: 0 }; }
        ).all();
        console.log("country_data", country_data);
  • ¶

    I initially sized the country dots with their area proportional to their population. However, either the smaller countries were too small to see or the larger countries were overwhelming. So, this is an exponential scale to make it easier to reasonably see all of the countries at once. It’s a lie, but hey, isn’t that what data visualization is… ;)

        var population_scale = d3.scale.pow()
            .domain([1, d3.max(gdp_data, (d) => d.population)])
            .range([3, 3000])
            .exponent(0.7);
  • ¶

    Create a c3.Plot for the country scatter plot.

        var average_gdp_layer: c3.Plot.Layer.Line.Horizontal<number>;
        var average_gdp_per_capita_layer: c3.Plot.Layer.Line.Vertical<number>;
        charts.push(new c3.Plot({
            anchor: '#worldbank_country_scatterplot',
            height: 330,
  • ¶

    Setup the scales as logarithmic for this example. Vertical is based on the country’s GDP while horizontal is based on GDP per capita.

            h: d3.scale.log().domain([1, d3.max(gdp_data, (d) => d.gdp_per_capita)]),
            v: d3.scale.log().domain([1, d3.max(gdp_data, (d) => d.gdp)]),
  • ¶

    Setup margins and allow the country dots to overflow into the margins.

            margins: {
                top: 20,
            },
            crop_margins: false,
  • ¶

    Add axes. In this case just draw the grid lines and axis label, but disable the tick marks and unit labels.

            axes: [
                new c3.Axis.X({
                    label: "GDP per capita",
                    grid: true,
                    tick_label: false,
                    tick_size: 0,
                }),
                new c3.Axis.Y({
                    label: "GDP",
                    grid: true,
                    tick_label: false,
                    tick_size: 0,
                }),
            ],
  • ¶

    This plot only contains a single scatter layer for the scatter plot.

            layers: [
                new c3.Plot.Layer.Scatter<CrossFilter.Grouping<string, CountryData>>({
  • ¶

    Bind the layer data to the country_data prepared above and only draw the country if it has data using filter. The key is used to uniquely identify elements which helps optimize some operations and provides consistency for animations and decimation.

                    data: country_data,
                    key: (d) => d.key,
                    filter: (d) => d.value.gdp_per_capita > 1,
  • ¶

    Set the x and y values based on GDP and GDP per capita.

                    x: (d) => Math.max(1, d.value.gdp_per_capita),
                    y: (d) => Math.max(1, d.value.gdp),
  • ¶

    Set the area of the circle based on the country’s population.

                    a: (d) => population_scale(Math.max(1, d.value.population)),
  • ¶

    circle_options.styles will set CSS styles for the circles. Here we set the fill and stroke color based on the country’s region. We also set the fill as translucent unless this country matches the country or region the user is hovering over with their mouse.

                    circle_options: {
                        styles: {
                            'fill': (d) => region_color(countries[d.key].region_id),
                            'stroke': (d) => region_color(countries[d.key].region_id),
                            'fill-opacity': (d) =>
                                hover_country === countries[d.key] || hover_region === regions[countries[d.key].region_id] ? 1 : 0.5,
                        },
  • ¶

    circle_options.events establishes event handlers for the circles. Here we setup handlers when the user hovers over the dot with the mouse. It sets the hover_country and restyles all the charts to reflect this. It also populates an informational div in the right column to display information about this particular country.

                        events: {
                            mouseenter: function(d) {
                                d3.select(this).style('stroke-width', 5);
                                d3.select('#hover_country_info').html("\
                                    <b>Country:</b> "+ countries[d.key].name + "<br/>\
                                   <b>Population:</b> "+ d.value.population + "<br/>\
                                   <b>GDP:</b> $"+ Math.floor(Math.abs(d.value.gdp)) + " billion<br/>\
                                   <b>GDP per capita:</b> $"+ Math.floor(Math.abs(d.value.gdp_per_capita)));
                                hover_country = countries[d.key];
                                restyle();
                            },
                            mouseleave: function() {
                                d3.select(this).style('stroke-width', 1);
                                d3.select('#hover_country_info').html("<i>Hover over dot to view country info.</i>");
                                hover_country = null;
                                restyle();
                            },
                        },
                    },
  • ¶

    Enable animations in this plot so the countries will move smoothly as the year filter is updated.

                    point_options: {
                        animate: true,
                        duration: 200,
                    },
                }),
  • ¶

    Add a horizontal line layer for the average GDP. Note how the data is actually set in the redraw_start event below.

                average_gdp_layer = new c3.Plot.Layer.Line.Horizontal<number>({
                    label_options: {
                        text: (avg) => "Avg GDP: $" + d3.format(',')(Math.floor(avg)) + "b",
                        dx: '1em',
                    },
                    vector_options: {
                        styles: { stroke: 'purple' },
                        animate: true,
                        duration: 500,
                    },
                }),
  • ¶

    Add a vertical line layer for the average GDP per capita.

                average_gdp_per_capita_layer = new c3.Plot.Layer.Line.Vertical<number>({
                    vector_options: {
                        styles: {
                            stroke: (d, i) => i ? 'orange' : 'purple',
                        },
                        animate: true,
                        duration: 500,
                    },
                }),
            ],
  • ¶

    Setup an event handler on the chart which fires whenever the chart is redrawn to reflect new data, but before any built-in behaviour. This allows us to set the data for the average line layers before they are drawn based on the data set due to the current filters.

            handlers: {
                'redraw_start': function () {
                    var country_count = country_data.filter((d) => d.value.gdp_per_capita > 1).length;
                    if (country_count) {
                        var average_gdp = total_gdp.value() / country_count;
                        var average_gdp_per_capita_by_country = d3.sum(country_data, (d) => d.value.gdp_per_capita) / country_count;
                        var average_gdp_per_capita_by_pop = total_gdp.value() * 1000000000 / total_population.value();
                        average_gdp_layer.data = [average_gdp];
                        average_gdp_per_capita_layer.data = [average_gdp_per_capita_by_pop, average_gdp_per_capita_by_country];
                    } else {
                        average_gdp_layer.data = [];
                        average_gdp_per_capita_layer.data = [];
                    }
                },
            },
        }));
  • ¶

    ###########################################################################

    Urbanization Histogram

  • ¶

    For the urbanization chart we will display the total world GDP but broken down based on the percentage of the country that is urbanized (living in a city of 1 million or more). We’ll use the urban_dim Crossfilter dimension for this. Notice that in this case we provide a callback function to group(). Previous examples did not do this and just grouped the data based on the dimension’s data directly. Here we are saying that we want to actually group the data along that dimension. This callback effectively groups countries into groups of 5% increments based on their urbanization. So, all data for countries with [0-5) percent urbanization are put into one group, countries with [5-10) are put into a second group and so on.

        var gdp_by_urban_data = urban_dim
            .group((key) => Math.floor(key / 5) * 5)
            .reduceSum((d) => d.gdp)
            .all();
  • ¶

    Create the c3.Plot.Selectable for this histogram. The selectable: 'h' enables the user to make horizontal selections in the chart.

        charts.push(new c3.Plot.Selectable<CrossFilter.Grouping<number, number>>({
            anchor: '#worldbank_urbanization_histogram',
            height: 100,
            selectable: 'h',
    
            data: gdp_by_urban_data,
  • ¶

    Assign the class urban to this chart for styling using a stylesheet.

            class: 'urban',
  • ¶

    Setup the scales for the chart. The horizontal scale has a domain from 0-100 to cover the different urbanization percentages. With our dataset the data points will only actually fall at intervals of 5. Note that the domain isn’t set here for the vertical scale. It needs to be set before the chart is rendered, but we do that below in the redraw_start event handler.

            h: d3.scale.linear().domain([0, 100]),
            v: d3.scale.linear(),
  • ¶

    x and y accessors. With our data key is the urbanization percentage and value is the GDP.

            x: (d) => d.key,
            y: (d) => d.value > 1 ? d.value : 0,
  • ¶

    Setup margins to allow room for the chart labels

            margins: {
                top: 10,
                right: 20,
            },
            crop_margins: false,
  • ¶

    Add axes to provide labels and percentage ticks

            axes: [
                new c3.Axis.X({
                    label: "Urbanization %",
                }),
                new c3.Axis.Y({
                    label: "GDP",
                    tick_size: 0,
                    tick_label: false,
                }),
            ],
  • ¶

    Layers to render. This example looks silly, but is just to demonstrate that you can add multiple layers with the same data that are drawn differently. Here you can see an area layer drawn, a line layer on top of that to accentuate the top of the graph, and then a scatter layer on top of that to show a big dot for each datapoint. The cardinal interpolation ensures that the curves actually pass through the datapoint, which is not the case with basis curve interpolation.

            layers: [
                new c3.Plot.Layer.Area<CrossFilter.Grouping<number, number>>({
                    interpolate: 'cardinal',
                }),
                new c3.Plot.Layer.Line<CrossFilter.Grouping<number, number>>({
                    interpolate: 'cardinal',
                    r: 5,
                }),
            ],
  • ¶

    Add chart event handlers

            handlers: {
  • ¶

    redraw_start is called before C3 starts to actually draw the chart. We use it here to set the vertical scale each time the chart is drawn so the graph always goes to the top of the chart, no matter how the data is filtered.

                redraw_start: function () {
                    this.v.domain([0, d3.max(gdp_by_urban_data, (d) => d.value)]);
                },
  • ¶

    select is triggered whenever the user makes or modifies a selection in the chart. We use that here to filter out the data based on the urbanization dimension.

                select: function (extent) {
                    if (extent) {
                        extent[1]++;
                        urban_dim.filterRange(extent);
                    } else {
                        urban_dim.filterAll();
                    }
                    redraw();
                },
            },
        }));
  • ¶

    ###########################################################################

    Histograms

  • ¶

    Instead of adding the charts to a div node in the DOM, this section will demonstrate another way to layout charts in a table on a web page. You can use whatever HTML layout you prefer.

  • ¶

    This is a function that will create a row in the table with a label column and a column with the cooresponding C3 histogram based on the dimension and histogram data passed in.

        function histogram_plot(name: string,
            dimension: CrossFilter.Dimension<GDPData, number>,
            hist_data: CrossFilter.Grouping<number, number>[]) {
            var row = d3.selectAll('#worldbank_histogram_table').append('tr')
                .style('border-top', '1px solid black')
                .style('border-bottom', '1px solid black');
            row.append('td')
                .style('text-align', 'center')
                .style('font-weight', 'bold')
                .style('background-color', 'lightgray')
                .style('width', '5em')
                .text(name);
  • ¶

    Create new histogram chart returned by this factory function.

            return new c3.Plot.Selectable<CrossFilter.Grouping<number, number>>({
                anchor: <HTMLElement>row.append('td').append('div').node(),
                height: 75,
                selectable: 'h',
    
                data: hist_data,
  • ¶

    Scales for the histogram. The horizontal scale goes from 0-100% for the histogram. The vertical scale reflects the count of countries that fall in that percentage, though it is set below in the redraw_start handler.

                h: d3.scale.linear().domain([0, 100]),
                v: d3.scale.linear(),
  • ¶

    x and y accessors. key represents the percentage of GDP based on this dimension and value represents the count of countries with that percentage.

                x: (d) => d.key,
                y: (d) => d.value,
  • ¶

    Notice there are no axes added here, that is covered below.

  • ¶

    Add a layer to draw an area graph for the histogram. By assigning it a class based on the dimension name the stylesheet will specify how it is styled. It also allows these layers to be highlighted when the legend at the top of the example is hovered. The cardinal interpolation ensures that the curve actually intersects each datapoint.

                layers: [
                    new c3.Plot.Layer.Area<CrossFilter.Grouping<number, number>>({
                        class: name,
                        interpolate: 'cardinal',
                    }),
                ],
  • ¶

    Add chart event handlers

                handlers: {
  • ¶

    redraw_start is called before C3 starts to actually draw the chart. We use it here to set the vertical scale each time the chart is drawn so the graph always goes to the top of the chart, no matter how the data is filtered.

                    redraw_start: function () {
                        this.v.domain([0, d3.max(hist_data, (d) => d.value)]);
                    },
  • ¶

    select is triggered whenever the user makes or modifies a selection in the chart. We use that here to filter out the data based on this dimension.

                    select: function (extent) {
                        if (extent) {
                            extent[1]++;
                            dimension.filterRange(extent);
                        } else {
                            dimension.filterAll();
                        }
                        redraw();
                    },
                },
            });
        }
  • ¶

    For these histograms we will display a histogram of the count of countries based on the percentage of their GDP based on services, agriculture, etc. Notice that these groupings are missing calls to reduceSum(). Therefore they use the default reduction which just counts the number of data elements in each group. This effectively gives us a histogram. The grouping function here will also group countries into groups of 5% just like the urbanization example.

        var services_hist = services_dim.group((key) => Math.floor(key / 5) * 5).all();
        var agriculture_hist = agriculture_dim.group((key) => Math.floor(key / 5) * 5).all();
        var manufacturing_hist = manufacturing_dim.group((key) => Math.floor(key / 5) * 5).all();
  • ¶

    Create three rows in the table with histogram charts for each of these dimensions.

        charts.push(histogram_plot('services', services_dim, services_hist));
        charts.push(histogram_plot('manufacturing', manufacturing_dim, manufacturing_hist));
        charts.push(histogram_plot('agriculture', agriculture_dim, agriculture_hist));
  • ¶

    Previous examples usually attached axes directly to their associated plot. This example demonstrates a different way of laying out the charts. To provide flexibility, axes can be created independently of plots. This example creates a single X-Axis that is added to the table and thus shared by all three histograms.

        var axis_row = d3.selectAll('#worldbank_histogram_table').append('tr');
        axis_row.append('td');
        charts.push(new c3.Axis.X({
            anchor: <HTMLElement>axis_row.append('td').append('div').node(),
            height: 30,
            scale: d3.scale.linear().domain([0, 100]),
        }));
  • ¶

    #########################################################################################

    Country Table

  • ¶

    Create a second dimension to filter on so the scatter plot will reflect this filter.

        var country_dim2 = xf.dimension((d) => d.country_code);
        var country_data2 = country_dim2.group().reduce<CountryData>(
            function (p, v) {
                p.gdp += v.gdp;
                p.gdp_per_capita += v.gdp_per_capita;
                p.population += v.population;
                p.land_area += v.land_area;
                return p;
            },
            function (p, v) {
                p.gdp -= v.gdp;
                p.gdp_per_capita -= v.gdp_per_capita;
                p.population -= v.population;
                p.land_area -= v.land_area;
                return p;
            },
            function () { return { gdp: 0, gdp_per_capita: 0, population: 0, land_area: 0 }; }
        ).all();
  • ¶

    Create a c3.Table to show the top countries

        var country_gdp_column: c3.Table.Column<CrossFilter.Grouping<string, CountryData>>;
        charts.push(new c3.Table<CrossFilter.Grouping<string, CountryData>>({
            anchor: '#worldbank_country_table',
            width: '100%',
            data: country_data2,
  • ¶

    Enable users to select and sort countries in this table.

            selectable: true,
            sortable: true,
  • ¶

    Limit this table to only show the top 10 countries and not to display any countries that have a 0 value.

            limit_rows: 10,
            filter: (d) => d.value.gdp > 0.000000001,
  • ¶

    Divide the table up into pages and allow user to search the table

            pagination: true,
            searchable: true,
  • ¶

    Setup columns for this table

            columns: [
                {
                    header: { text: "Country" },
                    cells: { html: (d) => '<b>' + countries[d.key].name + '</b>' },
                }, {
  • ¶

    The “Region” column is styled to have a background that matches the color for the region this country is in.

                    header: { text: "Region" },
                    cells: {
                        text: (d) => regions[countries[d.key].region_id].name,
                        styles: {
                            'background-color': (d) => region_color(countries[d.key].region_id),
                        },
                    },
                }, {
  • ¶

    These columns use custom formatters to set the html content of the cell adding commas, units, rounding, etc. You can add raw HTML if you like. These column also use the vis visualization support to render a bar graph based on the cell value.

                    header: { text: "Population" },
                    cells: { html: (d) => d3.format(',')(d.value.population) },
                    sort: (d) => d.value.population,
                    vis: 'bar',
                }, {
                    header: { text: "Land Area" },
                    cells: { html: (d) => (d3.format(',')(Math.round(d.value.land_area))) + "km<sup>2</sup>" },
                    sort: (d) => Math.round(d.value.land_area),
                    vis: 'bar',
                },
  • ¶

    Assign this column to a variable gdp_column so we can refer to it below for setting the default sort.

                country_gdp_column = {
                    header: { text: "GDP" },
                    cells: { html: (d) => "$" + (d3.format(',')(Math.round(d.value.gdp))) + "b" },
                    sort: (d) => Math.round(d.value.gdp),
                    vis: 'bar',
                }, {
  • ¶

    By default, the bar visualization will base the bar width as a percentage of the total value of all cells. This isn’t always appropriate, though, such as in the case of GDP per capita. So, here we manually set the total_value to be the maximum per capita value. Look out for Monaco!

                    header: { text: "GDP per capita" },
                    cells: { html: (d) => "$" + (d3.format(',')(Math.round(d.value.gdp_per_capita))) },
                    sort: (d) => Math.round(d.value.gdp_per_capita),
                    vis: 'bar',
                    total_value: function () { return d3.max(country_data2, (d) => d.value.gdp_per_capita); },
                },
            ],
  • ¶

    Initial column to sort on.

            sort_column: country_gdp_column,
  • ¶

    Add some padding to each cell using cell_options.styles to assign styles to cells

            cell_options: {
                styles: {
                    padding: "0 0.25em",
                },
            },
  • ¶

    Use row_options.classes to enable CSS classes on table rows. The classes can be whatever class you want for your styling. Here we turn on the hover class if this country matches the country the user is hovering over or is in the region the use is hovering over with their mouse. The stylesheet will then dictate how these rows appear; in this case giving them a wheat-colored background.

            row_options: {
                classes: {
                    hover: (d) => hover_country === countries[d.key] || hover_region === regions[countries[d.key].region_id],
                },
  • ¶

    row_options.events adds event handlers to table rows. These handlers set the global hover country when the user hovers the mouse over a row. restyle() is then called for all charts to update based on this new focus country.

                events: {
                    mouseenter: (d) => {
                        hover_country = countries[d.key];
                        restyle();
                    },
                    mouseleave: (d) => {
                        hover_country = null;
                        restyle();
                    },
                },
            },
  • ¶

    handlers adds event handlers to the chart object itself. Here we tie into the select event when the user selects one or more row to filter the data on only that country. Note: While all the other charts will filter to only display the filtered country the country scatter plot will still display all countries. This is because when you filter on a particular dimension it affects all other dimensinos, but not the one used to filter. This is actually a very good thing, otherwise when you filtered on a chart or table all of the data outside of that selection would then disappear which would be awkward and hard to change the selection. If we wanted the country scatter plot to only show the filtered country, the solution would be easy: just make another dimension and use different ones for the scatter plot and this table.

            handlers: {
                'select': (selections) => {
                    if (selections.length)
                        country_dim2.filter((key) => selections.map((d) => d.key).indexOf(key) >= 0);
                    else
                        country_dim2.filterAll();
                    redraw();
                },
  • ¶

    The match event is triggered for a row that is found with a user search

                'found': (search, d) => {
                    hover_country = d != null ? countries[d.key] : null;
                    restyle();
                },
            },
        }));
  • ¶

    #########################################################################################

    Initial Rendering

  • ¶

    Perform the initial render()

        for (let chart of charts)
            chart.render();
  • ¶

    Resize charts if the window is resized.

        window.onresize = function () {
            for (let chart of charts)
                chart.resize();
        }
    }
  • ¶

    #########################################################################################

    Start Here

  • ¶

    The loading and rendering are initiated here.. It calls the download_data() function to download the data which returns a Promise. We setup a then callback function to be called when all of the data is loaded and promised. This callback function calls which ends up calling the render() function.

    download_data().then(function () { setTimeout(render, 0); });
  • ¶

    The reason to have setTimeout call the render function instead of calling it directly is just a trick to help with debugging in the browser when using promises. The callback function for a promise is called, which catches all exceptions to be used to call an error callback. This is great except unhandled errors are then reported to the debugger later after we are already out of the context of the error. So, you can’t navigate the stack, view the contents of variables, etc. By using setTimeout it will cause the callback to return immediatly and then the browser will call render itself. Since this is done outside of the promise’s scope, exceptions are not caught and passed to the promise error handler and instead can be caught and handeled as normal in a debugger. In production code you could use this instead:

  • ¶
    download_data().then(render)