/** * The geo choropleth chart is designed as an easy way to create a crossfilter driven choropleth map * from GeoJson data. This chart implementation was inspired by * {@link http://bl.ocks.org/4060606 the great d3 choropleth example}. * * Examples: * - {@link http://dc-js.github.com/dc.js/vc/index.html US Venture Capital Landscape 2011} * @class geoChoroplethChart * @memberof dc * @mixes dc.colorMixin * @mixes dc.baseMixin * @example * // create a choropleth chart under '#us-chart' element using the default global chart group * var chart1 = dc.geoChoroplethChart('#us-chart'); * // create a choropleth chart under '#us-chart2' element using chart group A * var chart2 = dc.compositeChart('#us-chart2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.geoChoroplethChart} */ dc.geoChoroplethChart = function (parent, chartGroup) { var _chart = dc.colorMixin(dc.baseMixin({})); _chart.colorAccessor(function (d) { return d || 0; }); var _geoPath = d3.geoPath(); var _projectionFlag; var _projection; var _geoJsons = []; _chart._doRender = function () { _chart.resetSvg(); for (var layerIndex = 0; layerIndex < _geoJsons.length; ++layerIndex) { var states = _chart.svg().append('g') .attr('class', 'layer' + layerIndex); var regionG = states.selectAll('g.' + geoJson(layerIndex).name) .data(geoJson(layerIndex).data); regionG = regionG.enter() .append('g') .attr('class', geoJson(layerIndex).name) .merge(regionG); regionG .append('path') .attr('fill', 'white') .attr('d', _getGeoPath()); regionG.append('title'); plotData(layerIndex); } _projectionFlag = false; }; function plotData (layerIndex) { var data = generateLayeredData(); if (isDataLayer(layerIndex)) { var regionG = renderRegionG(layerIndex); renderPaths(regionG, layerIndex, data); renderTitle(regionG, layerIndex, data); } } function generateLayeredData () { var data = {}; var groupAll = _chart.data(); for (var i = 0; i < groupAll.length; ++i) { data[_chart.keyAccessor()(groupAll[i])] = _chart.valueAccessor()(groupAll[i]); } return data; } function isDataLayer (layerIndex) { return geoJson(layerIndex).keyAccessor; } function renderRegionG (layerIndex) { var regionG = _chart.svg() .selectAll(layerSelector(layerIndex)) .classed('selected', function (d) { return isSelected(layerIndex, d); }) .classed('deselected', function (d) { return isDeselected(layerIndex, d); }) .attr('class', function (d) { var layerNameClass = geoJson(layerIndex).name; var regionClass = dc.utils.nameToId(geoJson(layerIndex).keyAccessor(d)); var baseClasses = layerNameClass + ' ' + regionClass; if (isSelected(layerIndex, d)) { baseClasses += ' selected'; } if (isDeselected(layerIndex, d)) { baseClasses += ' deselected'; } return baseClasses; }); return regionG; } function layerSelector (layerIndex) { return 'g.layer' + layerIndex + ' g.' + geoJson(layerIndex).name; } function isSelected (layerIndex, d) { return _chart.hasFilter() && _chart.hasFilter(getKey(layerIndex, d)); } function isDeselected (layerIndex, d) { return _chart.hasFilter() && !_chart.hasFilter(getKey(layerIndex, d)); } function getKey (layerIndex, d) { return geoJson(layerIndex).keyAccessor(d); } function geoJson (index) { return _geoJsons[index]; } function renderPaths (regionG, layerIndex, data) { var paths = regionG .select('path') .attr('fill', function () { var currentFill = d3.select(this).attr('fill'); if (currentFill) { return currentFill; } return 'none'; }) .on('click', function (d) { return _chart.onClick(d, layerIndex); }); dc.transition(paths, _chart.transitionDuration(), _chart.transitionDelay()).attr('fill', function (d, i) { return _chart.getColor(data[geoJson(layerIndex).keyAccessor(d)], i); }); } _chart.onClick = function (d, layerIndex) { var selectedRegion = geoJson(layerIndex).keyAccessor(d); dc.events.trigger(function () { _chart.filter(selectedRegion); _chart.redrawGroup(); }); }; function renderTitle (regionG, layerIndex, data) { if (_chart.renderTitle()) { regionG.selectAll('title').text(function (d) { var key = getKey(layerIndex, d); var value = data[key]; return _chart.title()({key: key, value: value}); }); } } _chart._doRedraw = function () { for (var layerIndex = 0; layerIndex < _geoJsons.length; ++layerIndex) { plotData(layerIndex); if (_projectionFlag) { _chart.svg().selectAll('g.' + geoJson(layerIndex).name + ' path').attr('d', _getGeoPath()); } } _projectionFlag = false; }; /** * **mandatory** * * Use this function to insert a new GeoJson map layer. This function can be invoked multiple times * if you have multiple GeoJson data layers to render on top of each other. If you overlay multiple * layers with the same name the new overlay will override the existing one. * @method overlayGeoJson * @memberof dc.geoChoroplethChart * @instance * @see {@link http://geojson.org/ GeoJSON} * @see {@link https://github.com/topojson/topojson/wiki TopoJSON} * @see {@link https://github.com/topojson/topojson-1.x-api-reference/blob/master/API-Reference.md#wiki-feature topojson.feature} * @example * // insert a layer for rendering US states * chart.overlayGeoJson(statesJson.features, 'state', function(d) { * return d.properties.name; * }); * @param {geoJson} json - a geojson feed * @param {String} name - name of the layer * @param {Function} keyAccessor - accessor function used to extract 'key' from the GeoJson data. The key extracted by * this function should match the keys returned by the crossfilter groups. * @returns {dc.geoChoroplethChart} */ _chart.overlayGeoJson = function (json, name, keyAccessor) { for (var i = 0; i < _geoJsons.length; ++i) { if (_geoJsons[i].name === name) { _geoJsons[i].data = json; _geoJsons[i].keyAccessor = keyAccessor; return _chart; } } _geoJsons.push({name: name, data: json, keyAccessor: keyAccessor}); return _chart; }; /** * Gets or sets a custom geo projection function. See the available * {@link https://github.com/d3/d3-geo/blob/master/README.md#projections d3 geo projection functions}. * * Starting version 3.0 it has been deprecated to rely on the default projection being * {@link https://github.com/d3/d3-geo/blob/master/README.md#geoAlbersUsa d3.geoAlbersUsa()}. Please * set it explicitly. {@link https://bl.ocks.org/mbostock/5557726 * Considering that `null` is also a valid value for projection}, if you need * projection to be `null` please set it explicitly to `null`. * @method projection * @memberof dc.geoChoroplethChart * @instance * @see {@link https://github.com/d3/d3-geo/blob/master/README.md#projections d3.projection} * @see {@link https://github.com/d3/d3-geo-projection d3-geo-projection} * @param {d3.projection} [projection=d3.geoAlbersUsa()] * @returns {d3.projection|dc.geoChoroplethChart} */ _chart.projection = function (projection) { if (!arguments.length) { return _projection; } _projection = projection; _projectionFlag = true; return _chart; }; var _getGeoPath = function () { if (_projection === undefined) { dc.logger.warn('choropleth projection default of geoAlbers is deprecated,' + ' in next version projection will need to be set explicitly'); return _geoPath.projection(d3.geoAlbersUsa()); } return _geoPath.projection(_projection); }; /** * Returns all GeoJson layers currently registered with this chart. The returned array is a * reference to this chart's internal data structure, so any modification to this array will also * modify this chart's internal registration. * @method geoJsons * @memberof dc.geoChoroplethChart * @instance * @returns {Array<{name:String, data: Object, accessor: Function}>} */ _chart.geoJsons = function () { return _geoJsons; }; /** * Returns the {@link https://github.com/d3/d3-geo/blob/master/README.md#paths d3.geoPath} object used to * render the projection and features. Can be useful for figuring out the bounding box of the * feature set and thus a way to calculate scale and translation for the projection. * @method geoPath * @memberof dc.geoChoroplethChart * @instance * @see {@link https://github.com/d3/d3-geo/blob/master/README.md#paths d3.geoPath} * @returns {d3.geoPath} */ _chart.geoPath = function () { return _geoPath; }; /** * Remove a GeoJson layer from this chart by name * @method removeGeoJson * @memberof dc.geoChoroplethChart * @instance * @param {String} name * @returns {dc.geoChoroplethChart} */ _chart.removeGeoJson = function (name) { var geoJsons = []; for (var i = 0; i < _geoJsons.length; ++i) { var layer = _geoJsons[i]; if (layer.name !== name) { geoJsons.push(layer); } } _geoJsons = geoJsons; return _chart; }; return _chart.anchor(parent, chartGroup); };