// # 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_](http://promises-aplus.github.io/promises-spec/) // 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**](http://square.github.io/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().reduceSum((d) => d.population); var total_gdp = xf.groupAll().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>({ 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>({ 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>({ options: { title: "% Services", class: 'services', }, data: services_by_year_data, }), new c3.Plot.Layer.Line>({ options: { title: "% Manufacturing", class: 'manufacturing', }, data: manufacturing_by_year_data, }), new c3.Plot.Layer.Line>({ 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({ 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(); // Create a `c3.Table` for the regions table. var regions_table: c3.Table>; var gdp_column: c3.Table.Column>; charts.push(regions_table = new c3.Table>({ anchor: '#worldbank_region_table', // Bind it to the **data** prepared above. // The `key`'s represent region_id`s 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( 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; var average_gdp_per_capita_layer: c3.Plot.Layer.Line.Vertical; 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>({ // 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("\ Country: "+ countries[d.key].name + "
\ Population: "+ d.value.population + "
\ GDP: $"+ Math.floor(Math.abs(d.value.gdp)) + " billion
\ GDP per capita: $"+ 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("Hover over dot to view country info."); 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({ 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({ 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>({ 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>({ interpolate: 'cardinal', }), new c3.Plot.Layer.Line>({ 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, hist_data: CrossFilter.Grouping[]) { 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>({ anchor: 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>({ 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: 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( 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>; charts.push(new c3.Table>({ 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) => '' + countries[d.key].name + '' }, }, { // 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))) + "km2" }, 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)