import {geoPath, geoAlbersUsa} from 'd3-geo';
import {select} from 'd3-selection';
import {BaseMixin} from '../base/base-mixin';
import {ColorMixin} from '../base/color-mixin';
import {transition} from '../core/core';
import {logger} from '../core/logger';
import {events} from '../core/events';
import {utils} from '../core/utils';
import {d3compat} from '../core/config';
/**
* 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}
* @mixes ColorMixin
* @mixes BaseMixin
*/
export class GeoChoroplethChart extends ColorMixin(BaseMixin) {
/**
* Create a Geo Choropleth Chart.
* @example
* // create a choropleth chart under '#us-chart' element using the default global chart group
* var chart1 = new GeoChoroplethChart('#us-chart');
* // create a choropleth chart under '#us-chart2' element using chart group A
* var chart2 = new 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.
*/
constructor (parent, chartGroup) {
super();
this.colorAccessor(d => d || 0);
this._geoPath = geoPath();
this._projectionFlag = undefined;
this._projection = undefined;
this._geoJsons = [];
this.anchor(parent, chartGroup);
}
_doRender () {
this.resetSvg();
for (let layerIndex = 0; layerIndex < this._geoJsons.length; ++layerIndex) {
const states = this.svg().append('g')
.attr('class', `layer${layerIndex}`);
let regionG = states.selectAll(`g.${this._geoJson(layerIndex).name}`)
.data(this._geoJson(layerIndex).data);
regionG = regionG.enter()
.append('g')
.attr('class', this._geoJson(layerIndex).name)
.merge(regionG);
regionG
.append('path')
.classed('dc-tabbable', this._keyboardAccessible)
.attr('fill', 'white')
.attr('d', this._getGeoPath());
regionG.append('title');
this._plotData(layerIndex);
}
this._projectionFlag = false;
}
_plotData (layerIndex) {
const data = this._generateLayeredData();
if (this._isDataLayer(layerIndex)) {
const regionG = this._renderRegionG(layerIndex);
this._renderPaths(regionG, layerIndex, data);
this._renderTitles(regionG, layerIndex, data);
}
}
_generateLayeredData () {
const data = {};
const groupAll = this.data();
for (let i = 0; i < groupAll.length; ++i) {
data[this.keyAccessor()(groupAll[i])] = this.valueAccessor()(groupAll[i]);
}
return data;
}
_isDataLayer (layerIndex) {
return this._geoJson(layerIndex).keyAccessor;
}
_renderRegionG (layerIndex) {
const regionG = this.svg()
.selectAll(this._layerSelector(layerIndex))
.classed('selected', d => this._isSelected(layerIndex, d))
.classed('deselected', d => this._isDeselected(layerIndex, d))
.attr('class', d => {
const layerNameClass = this._geoJson(layerIndex).name;
const regionClass = utils.nameToId(this._geoJson(layerIndex).keyAccessor(d));
let baseClasses = `${layerNameClass} ${regionClass}`;
if (this._isSelected(layerIndex, d)) {
baseClasses += ' selected';
}
if (this._isDeselected(layerIndex, d)) {
baseClasses += ' deselected';
}
return baseClasses;
});
return regionG;
}
_layerSelector (layerIndex) {
return `g.layer${layerIndex} g.${this._geoJson(layerIndex).name}`;
}
_isSelected (layerIndex, d) {
return this.hasFilter() && this.hasFilter(this._getKey(layerIndex, d));
}
_isDeselected (layerIndex, d) {
return this.hasFilter() && !this.hasFilter(this._getKey(layerIndex, d));
}
_getKey (layerIndex, d) {
return this._geoJson(layerIndex).keyAccessor(d);
}
_geoJson (index) {
return this._geoJsons[index];
}
_renderPaths (regionG, layerIndex, data) {
const paths = regionG
.select('path')
.attr('fill', function () {
const currentFill = select(this).attr('fill');
if (currentFill) {
return currentFill;
}
return 'none';
})
.on('click', d3compat.eventHandler(d => this.onClick(d, layerIndex)));
if (this._keyboardAccessible) {
this._makeKeyboardAccessible(this.onClick, layerIndex);
}
transition(paths, this.transitionDuration(),
this.transitionDelay()).attr('fill', (d, i) => this.getColor(data[this._geoJson(layerIndex).keyAccessor(d)], i));
}
onClick (d, layerIndex) {
const selectedRegion = this._geoJson(layerIndex).keyAccessor(d);
events.trigger(() => {
this.filter(selectedRegion);
this.redrawGroup();
});
}
_renderTitles (regionG, layerIndex, data) {
if (this.renderTitle()) {
regionG.selectAll('title').text(d => {
const key = this._getKey(layerIndex, d);
const value = data[key];
return this.title()({key: key, value: value});
});
}
}
_doRedraw () {
for (let layerIndex = 0; layerIndex < this._geoJsons.length; ++layerIndex) {
this._plotData(layerIndex);
if (this._projectionFlag) {
this.svg().selectAll(`g.${this._geoJson(layerIndex).name} path`).attr('d', this._getGeoPath());
}
}
this._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.
* @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 {GeoChoroplethChart}
*/
overlayGeoJson (json, name, keyAccessor) {
for (let i = 0; i < this._geoJsons.length; ++i) {
if (this._geoJsons[i].name === name) {
this._geoJsons[i].data = json;
this._geoJsons[i].keyAccessor = keyAccessor;
return this;
}
}
this._geoJsons.push({name: name, data: json, keyAccessor: keyAccessor});
return this;
}
/**
* 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`.
* @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|GeoChoroplethChart}
*/
projection (projection) {
if (!arguments.length) {
return this._projection;
}
this._projection = projection;
this._projectionFlag = true;
return this;
}
_getGeoPath () {
if (this._projection === undefined) {
logger.warn('choropleth projection default of geoAlbers is deprecated,' +
' in next version projection will need to be set explicitly');
return this._geoPath.projection(geoAlbersUsa());
}
return this._geoPath.projection(this._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.
* @returns {Array<{name:String, data: Object, accessor: Function}>}
*/
geoJsons () {
return this._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.
* @see {@link https://github.com/d3/d3-geo/blob/master/README.md#paths d3.geoPath}
* @returns {d3.geoPath}
*/
geoPath () {
return this._geoPath;
}
/**
* Remove a GeoJson layer from this chart by name
* @param {String} name
* @returns {GeoChoroplethChart}
*/
removeGeoJson (name) {
const geoJsons = [];
for (let i = 0; i < this._geoJsons.length; ++i) {
const layer = this._geoJsons[i];
if (layer.name !== name) {
geoJsons.push(layer);
}
}
this._geoJsons = geoJsons;
return this;
}
}
export const geoChoroplethChart = (parent, chartGroup) => new GeoChoroplethChart(parent, chartGroup);