New chart type : Chord Diagrams (#3013)

This commit is contained in:
Maxime Beauchemin 2017-06-26 16:44:47 -07:00 committed by GitHub
parent a55f963e52
commit 7045018d86
8 changed files with 194 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

View File

@ -334,11 +334,14 @@ export const controls = {
type: 'SelectControl', type: 'SelectControl',
multi: true, multi: true,
label: 'Columns', label: 'Columns',
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.gb_cols : [],
}),
default: [], default: [],
description: 'One or many controls to pivot as columns', description: 'One or many controls to pivot as columns',
optionRenderer: c => <ColumnOption column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: state => ({
options: (state.datasource) ? state.datasource.columns : [],
}),
}, },
all_columns: { all_columns: {

View File

@ -1,5 +1,7 @@
import { D3_TIME_FORMAT_OPTIONS } from './controls'; import { D3_TIME_FORMAT_OPTIONS } from './controls';
import * as v from '../validators';
export const sections = { export const sections = {
druidTimeSeries: { druidTimeSeries: {
label: 'Time', label: 'Time',
@ -635,6 +637,37 @@ const visTypes = {
}, },
}, },
}, },
chord: {
label: 'Chord Diagram',
controlPanelSections: [
{
label: null,
controlSetRows: [
['groupby', 'columns'],
['metric'],
['row_limit', 'y_axis_format'],
],
},
],
controlOverrides: {
y_axis_format: {
label: 'Number format',
description: 'Choose a number format',
},
groupby: {
label: 'Source',
multi: false,
validators: [v.nonEmpty],
description: 'Choose a source',
},
columns: {
label: 'Target',
multi: false,
validators: [v.nonEmpty],
description: 'Choose a target',
},
},
},
country_map: { country_map: {
label: 'Country Map', label: 'Country Map',
controlPanelSections: [ controlPanelSections: [

View File

@ -65,7 +65,7 @@
"react-ace": "^5.0.1", "react-ace": "^5.0.1",
"react-addons-css-transition-group": "^15.6.0", "react-addons-css-transition-group": "^15.6.0",
"react-addons-shallow-compare": "^15.4.2", "react-addons-shallow-compare": "^15.4.2",
"react-alert": "^2.0.1", "react-alert": "^1.0.14",
"react-bootstrap": "^0.31.0", "react-bootstrap": "^0.31.0",
"react-bootstrap-table": "^3.1.7", "react-bootstrap-table": "^3.1.7",
"react-dom": "^15.5.1", "react-dom": "^15.5.1",

View File

@ -0,0 +1,17 @@
.chord svg #circle circle {
fill: none;
pointer-events: all;
}
.chord svg .group path {
fill-opacity: .6;
}
.chord svg path.chord {
stroke: #000;
stroke-width: .25px;
}
.chord svg #circle:hover path.fade {
opacity: 0.2;
}

View File

@ -0,0 +1,101 @@
/* eslint-disable no-param-reassign */
import d3 from 'd3';
import { category21 } from '../javascripts/modules/colors';
import './chord.css';
function chordViz(slice, json) {
slice.container.html('');
const div = d3.select(slice.selector);
const nodes = json.data.nodes;
const fd = slice.formData;
const f = d3.format(fd.y_axis_format);
const width = slice.width();
const height = slice.height();
const outerRadius = Math.min(width, height) / 2 - 10;
const innerRadius = outerRadius - 24;
let chord;
const arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const layout = d3.layout.chord()
.padding(0.04)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);
const path = d3.svg.chord()
.radius(innerRadius);
const svg = div.append('svg')
.attr('width', width)
.attr('height', height)
.on('mouseout', () => chord.classed('fade', false))
.append('g')
.attr('id', 'circle')
.attr('transform', `translate(${width / 2}, ${height / 2})`);
svg.append('circle')
.attr('r', outerRadius);
// Compute the chord layout.
layout.matrix(json.data.matrix);
const group = svg.selectAll('.group')
.data(layout.groups)
.enter().append('g')
.attr('class', 'group')
.on('mouseover', (d, i) => {
chord.classed('fade', p => p.source.index !== i && p.target.index !== i);
});
// Add a mouseover title.
group.append('title').text((d, i) => `${nodes[i]}: ${f(d.value)}`);
// Add the group arc.
const groupPath = group.append('path')
.attr('id', (d, i) => 'group' + i)
.attr('d', arc)
.style('fill', (d, i) => category21(nodes[i]));
// Add a text label.
const groupText = group.append('text')
.attr('x', 6)
.attr('dy', 15);
groupText.append('textPath')
.attr('xlink:href', (d, i) => `#group${i}`)
.text((d, i) => nodes[i]);
// Remove the labels that don't fit. :(
groupText.filter(function (d, i) {
return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength();
})
.remove();
// Add the chords.
chord = svg.selectAll('.chord')
.data(layout.chords)
.enter().append('path')
.attr('class', 'chord')
.on('mouseover', (d) => {
chord.classed('fade', p => p !== d);
})
.style('fill', d => category21(nodes[d.source.index]))
.attr('d', path);
// Add an elaborate mouseover title for each chord.
chord.append('title').text(function (d) {
return nodes[d.source.index]
+ ' → ' + nodes[d.target.index]
+ ': ' + f(d.source.value)
+ '\n' + nodes[d.target.index]
+ ' → ' + nodes[d.source.index]
+ ': ' + f(d.target.value);
});
}
module.exports = chordViz;

View File

@ -10,6 +10,7 @@ const vizMap = {
cal_heatmap: require('./cal_heatmap.js'), cal_heatmap: require('./cal_heatmap.js'),
compare: require('./nvd3_vis.js'), compare: require('./nvd3_vis.js'),
directed_force: require('./directed_force.js'), directed_force: require('./directed_force.js'),
chord: require('./chord.jsx'),
dist_bar: require('./nvd3_vis.js'), dist_bar: require('./nvd3_vis.js'),
filter_box: require('./filter_box.jsx'), filter_box: require('./filter_box.jsx'),
heatmap: require('./heatmap.js'), heatmap: require('./heatmap.js'),

View File

@ -16,6 +16,7 @@ import uuid
import zlib import zlib
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from itertools import product
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pandas as pd import pandas as pd
@ -1231,6 +1232,39 @@ class DirectedForceViz(BaseViz):
return df.to_dict(orient='records') return df.to_dict(orient='records')
class ChordViz(BaseViz):
"""A Chord diagram"""
viz_type = "chord"
verbose_name = _("Directed Force Layout")
credits = '<a href="https://github.com/d3/d3-chord">Bostock</a>'
is_timeseries = False
def query_obj(self):
qry = super(ChordViz, self).query_obj()
fd = self.form_data
qry['groupby'] = [fd.get('groupby'), fd.get('columns')]
qry['metrics'] = [fd.get('metric')]
return qry
def get_data(self, df):
df.columns = ['source', 'target', 'value']
# Preparing a symetrical matrix like d3.chords calls for
nodes = list(set(df['source']) | set(df['target']))
matrix = {}
for source, target in product(nodes, nodes):
matrix[(source, target)] = 0
for source, target, value in df.to_records(index=False):
matrix[(source, target)] = value
m = [[matrix[(n1, n2)] for n1 in nodes] for n2 in nodes]
return {
'nodes': list(nodes),
'matrix': m,
}
class CountryMapViz(BaseViz): class CountryMapViz(BaseViz):
"""A country centric""" """A country centric"""
@ -1574,6 +1608,7 @@ viz_types_list = [
DirectedForceViz, DirectedForceViz,
SankeyViz, SankeyViz,
CountryMapViz, CountryMapViz,
ChordViz,
WorldMapViz, WorldMapViz,
FilterBoxViz, FilterBoxViz,
IFrameViz, IFrameViz,