Skip to content

Commit

Permalink
New chart type : Chord Diagrams
Browse files Browse the repository at this point in the history
  • Loading branch information
mistercrunch committed Jun 21, 2017
1 parent bd706eb commit 52b86d8
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 4 deletions.
Binary file added superset/assets/images/viz_thumbnails/chord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,14 @@ export const controls = {
type: 'SelectControl',
multi: true,
label: 'Columns',
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.gb_cols : [],
}),
default: [],
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: {
Expand Down
33 changes: 33 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { D3_TIME_FORMAT_OPTIONS } from './controls';

import * as v from '../validators';

export const sections = {
druidTimeSeries: {
label: 'Time',
Expand Down Expand Up @@ -622,6 +624,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: {
label: 'Country Map',
controlPanelSections: [
Expand Down
2 changes: 1 addition & 1 deletion superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"react-ace": "^5.0.1",
"react-addons-css-transition-group": "^15.6.0",
"react-addons-shallow-compare": "^15.4.2",
"react-alert": "^2.0.1",
"react-alert": "^1.0.14",
"react-bootstrap": "^0.31.0",
"react-bootstrap-table": "^3.1.7",
"react-dom": "^15.5.1",
Expand Down
17 changes: 17 additions & 0 deletions superset/assets/visualizations/chord.css
Original file line number Diff line number Diff line change
@@ -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;
}
101 changes: 101 additions & 0 deletions superset/assets/visualizations/chord.jsx
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions superset/assets/visualizations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const vizMap = {
cal_heatmap: require('./cal_heatmap.js'),
compare: require('./nvd3_vis.js'),
directed_force: require('./directed_force.js'),
chord: require('./chord.jsx'),
dist_bar: require('./nvd3_vis.js'),
filter_box: require('./filter_box.jsx'),
heatmap: require('./heatmap.js'),
Expand Down
35 changes: 35 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import zlib

from collections import OrderedDict, defaultdict
from itertools import product
from datetime import datetime, timedelta

import pandas as pd
Expand Down Expand Up @@ -1231,6 +1232,39 @@ def get_data(self, df):
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):

"""A country centric"""
Expand Down Expand Up @@ -1574,6 +1608,7 @@ def get_data(self, df):
DirectedForceViz,
SankeyViz,
CountryMapViz,
ChordViz,
WorldMapViz,
FilterBoxViz,
IFrameViz,
Expand Down

0 comments on commit 52b86d8

Please sign in to comment.