-
Notifications
You must be signed in to change notification settings - Fork 13.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New chart type : Chord Diagrams #3013
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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', | ||
|
@@ -622,6 +624,37 @@ const visTypes = { | |
}, | ||
}, | ||
}, | ||
chord: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file contains configuration of the new
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Could I please ask some questiones? I want to show a metric and some filter in the superset dashboard, after reading some articles, i am confused because the configuration needed to be changed does not exist in the files. For example, |
||
label: 'Chord Diagram', | ||
controlPanelSections: [ | ||
{ | ||
label: null, | ||
controlSetRows: [ | ||
['groupby', 'columns'], | ||
['metric'], | ||
['row_limit', 'y_axis_format'], | ||
], | ||
}, | ||
], | ||
controlOverrides: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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: [ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
.chord svg #circle circle { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by convention each visualization has its own |
||
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; | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is the heart of this new visualization, the framework will pass it a The json objects corresponds to the data returned from running your query and related context. To see exactly what you should expect in here you can click the The slice object contains, amongst other things, a reference to the dom element that you should mutate. |
||
slice.container.html(''); | ||
|
||
const div = d3.select(slice.selector); | ||
const nodes = json.data.nodes; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const fd = slice.formData; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On of the things that the slice object exposes isf the information provided by the user controls. The keys of this |
||
const f = d3.format(fd.y_axis_format); | ||
|
||
const width = slice.width(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The slice object exposes |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that function should be exported as the default export for the visualization module |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. simply adding the mapping from the |
||
dist_bar: require('./nvd3_vis.js'), | ||
filter_box: require('./filter_box.jsx'), | ||
heatmap: require('./heatmap.js'), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
import zlib | ||
|
||
from collections import OrderedDict, defaultdict | ||
from itertools import product | ||
from datetime import datetime, timedelta | ||
|
||
import pandas as pd | ||
|
@@ -1231,6 +1232,39 @@ def get_data(self, df): | |
return df.to_dict(orient='records') | ||
|
||
|
||
class ChordViz(BaseViz): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the backend part of this component, you can derive |
||
|
||
"""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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to override |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Now whatever |
||
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""" | ||
|
@@ -1574,6 +1608,7 @@ def get_data(self, df): | |
DirectedForceViz, | ||
SankeyViz, | ||
CountryMapViz, | ||
ChordViz, | ||
WorldMapViz, | ||
FilterBoxViz, | ||
IFrameViz, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file (
controls.jsx
) holds configuration for a "pool" of controls that can be used across visualizations. When creating a new visualization, you'll want to reuse controls that already exist. If the exact control that you need doesn't exist already, you'll want to create it here.Note that
type
references controls React components defined injavascripts/explore/component/controls/
.Also note that controls can be dynamic with
mapStateToProps
, allowing to map anything from the app's state into a prop for the component. This allows in this case to get configuration elements proper to the datasource in this case, but can also be used to change a control based on another control's value.