Simplifying the viz interface (#2005)

This commit is contained in:
Maxime Beauchemin 2017-01-24 14:03:17 -08:00 committed by GitHub
parent 1c338ba742
commit e46ba2b4a4
26 changed files with 1899 additions and 2192 deletions

View File

@ -1,6 +1,7 @@
/* eslint camelcase: 0 */
const $ = window.$ = require('jquery');
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
import { getExploreUrl } from '../exploreUtils';
export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
export function setDatasourceType(datasourceType) {
@ -89,13 +90,18 @@ export function chartUpdateStarted() {
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
export function chartUpdateSucceeded(query) {
return { type: CHART_UPDATE_SUCCEEDED, query };
export function chartUpdateSucceeded(queryResponse) {
return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
}
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
export function chartUpdateFailed(error, query) {
return { type: CHART_UPDATE_FAILED, error, query };
export function chartUpdateFailed(queryResponse) {
return { type: CHART_UPDATE_FAILED, queryResponse };
}
export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
export function chartRenderingFailed(error) {
return { type: CHART_RENDERING_FAILED, error };
}
export const UPDATE_EXPLORE_ENDPOINTS = 'UPDATE_EXPLORE_ENDPOINTS';
@ -167,3 +173,16 @@ export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS';
export function updateChartStatus(status) {
return { type: UPDATE_CHART_STATUS, status };
}
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, datasourceType) {
return function (dispatch) {
dispatch(updateChartStatus('loading'));
const url = getExploreUrl(formData, datasourceType, 'json');
$.getJSON(url, function (queryResponse) {
dispatch(chartUpdateSucceeded(queryResponse));
}).fail(function (err) {
dispatch(chartUpdateFailed(err));
});
};
}

View File

@ -23,12 +23,8 @@ const propTypes = {
viz_type: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
containerId: PropTypes.string.isRequired,
json_endpoint: PropTypes.string.isRequired,
csv_endpoint: PropTypes.string.isRequired,
standalone_endpoint: PropTypes.string.isRequired,
query: PropTypes.string,
column_formats: PropTypes.object,
data: PropTypes.any,
chartStatus: PropTypes.string,
isStarred: PropTypes.bool.isRequired,
chartUpdateStartTime: PropTypes.number.isRequired,
@ -37,88 +33,59 @@ const propTypes = {
table_name: PropTypes.string,
};
class ChartContainer extends React.Component {
class ChartContainer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
selector: `#${props.containerId}`,
mockSlice: {},
viz: {},
};
}
componentWillMount() {
const mockSlice = this.getMockedSliceObject(this.props);
this.setState({
mockSlice,
viz: visMap[this.props.viz_type](mockSlice),
});
}
componentDidMount() {
this.state.viz.render();
}
componentWillReceiveProps(nextProps) {
if (nextProps.chartStatus === 'loading') {
const mockSlice = this.getMockedSliceObject(nextProps);
this.setState({
mockSlice,
viz: visMap[nextProps.viz_type](mockSlice),
});
renderViz() {
const mockSlice = this.getMockedSliceObject();
try {
visMap[this.props.viz_type](mockSlice, this.props.queryResponse);
this.setState({ mockSlice });
} catch (e) {
this.props.actions.chartRenderingFailed(e);
}
}
componentDidUpdate(prevProps) {
if (this.props.chartStatus === 'loading') {
this.state.viz.render();
}
if (prevProps.height !== this.props.height) {
this.state.viz.resize();
if (
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height
) {
this.renderViz();
}
}
getMockedSliceObject(props) {
getMockedSliceObject() {
const props = this.props;
return {
viewSqlQuery: props.query,
data: {
csv_endpoint: props.csv_endpoint,
json_endpoint: props.json_endpoint,
standalone_endpoint: props.standalone_endpoint,
},
containerId: props.containerId,
jsonEndpoint: () => props.json_endpoint,
selector: this.state.selector,
container: {
html: (data) => {
// this should be a callback to clear the contents of the slice container
$(this.state.selector).html(data);
},
css: (dim, size) => {
// dimension can be 'height'
// pixel string can be '300px'
// should call callback to adjust height of chart
$(this.state.selector).css(dim, size);
},
height: () => parseInt(this.props.height, 10) - 100,
show: () => { this.render(); },
height: () => parseInt(props.height, 10) - 100,
show: () => { },
get: (n) => ($(this.state.selector).get(n)),
find: (classname) => ($(this.state.selector).find(classname)),
},
width: () => this.chartContainerRef.getBoundingClientRect().width,
height: () => parseInt(this.props.height, 10) - 100,
selector: this.state.selector,
height: () => parseInt(props.height, 10) - 100,
setFilter: () => {
// set filter according to data in store
@ -130,32 +97,25 @@ class ChartContainer extends React.Component {
{}
),
done: (payload) => {
// finished rendering callback
// Todo: end timer and chartLoading set to success
props.actions.chartUpdateSucceeded(payload.query);
},
done: () => {},
clearError: () => {
// no need to do anything here since Alert is closable
// query button will also remove Alert
},
error(msg) {
let payload = { error: msg };
try {
payload = JSON.parse(msg);
} catch (e) {
// pass
}
props.actions.chartUpdateFailed(payload.error, payload.query);
},
error() {},
d3format: (col, number) => {
// mock d3format function in Slice object in superset.js
const format = props.column_formats[col];
return d3format(format, number);
},
data: {
csv_endpoint: props.queryResponse.csv_endpoint,
json_endpoint: props.queryResponse.json_endpoint,
standalone_endpoint: props.queryResponse.standalone_endpoint,
},
};
}
@ -199,7 +159,7 @@ class ChartContainer extends React.Component {
}
<div
id={this.props.containerId}
ref={(ref) => { this.chartContainerRef = ref; }}
ref={ref => { this.chartContainerRef = ref; }}
className={this.props.viz_type}
style={{
opacity: loading ? '0.25' : '1',
@ -251,11 +211,13 @@ class ChartContainer extends React.Component {
state={CHART_STATUS_MAP[this.props.chartStatus]}
style={{ fontSize: '10px', marginRight: '5px' }}
/>
<ExploreActionButtons
slice={this.state.mockSlice}
canDownload={this.props.can_download}
query={this.props.query}
/>
{this.state.mockSlice &&
<ExploreActionButtons
slice={this.state.mockSlice}
canDownload={this.props.can_download}
query={this.props.queryResponse.query}
/>
}
</div>
</div>
}
@ -276,18 +238,15 @@ function mapStateToProps(state) {
slice_name: state.viz.form_data.slice_name,
viz_type: state.viz.form_data.viz_type,
can_download: state.can_download,
csv_endpoint: state.viz.csv_endpoint,
json_endpoint: state.viz.json_endpoint,
standalone_endpoint: state.viz.standalone_endpoint,
chartUpdateStartTime: state.chartUpdateStartTime,
chartUpdateEndTime: state.chartUpdateEndTime,
query: state.viz.query,
column_formats: state.viz.column_formats,
data: state.viz.data,
chartStatus: state.chartStatus,
isStarred: state.isStarred,
alert: state.chartAlert,
table_name: state.viz.form_data.datasource_name,
queryResponse: state.queryResponse,
};
}

View File

@ -30,6 +30,7 @@ class ExploreViewContainer extends React.Component {
componentDidMount() {
window.addEventListener('resize', this.handleResize.bind(this));
this.runQuery();
}
componentWillReceiveProps(nextProps) {
@ -38,7 +39,7 @@ class ExploreViewContainer extends React.Component {
&& autoQueryFields.indexOf(field) !== -1)
);
if (refreshChart) {
this.onQuery(nextProps.form_data);
this.onQuery();
}
}
@ -46,12 +47,12 @@ class ExploreViewContainer extends React.Component {
window.removeEventListener('resize', this.handleResize.bind(this));
}
onQuery(form_data) {
this.props.actions.chartUpdateStarted();
onQuery() {
this.runQuery();
history.pushState(
{},
document.title,
getExploreUrl(form_data, this.props.datasource_type)
getExploreUrl(this.props.form_data, this.props.datasource_type)
);
// remove alerts when query
this.props.actions.removeControlPanelAlert();
@ -63,6 +64,11 @@ class ExploreViewContainer extends React.Component {
return `${window.innerHeight - navHeight}px`;
}
runQuery() {
this.props.actions.runQuery(this.props.form_data, this.props.datasource_type);
}
handleResize() {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
@ -118,7 +124,7 @@ class ExploreViewContainer extends React.Component {
<div className="col-sm-4">
<QueryAndSaveBtns
canAdd="True"
onQuery={this.onQuery.bind(this, this.props.form_data)}
onQuery={this.onQuery.bind(this)}
onSave={this.toggleModal.bind(this)}
disabled={this.props.chartStatus === 'loading'}
errorMessage={this.renderErrorMessage()}
@ -128,7 +134,7 @@ class ExploreViewContainer extends React.Component {
actions={this.props.actions}
form_data={this.props.form_data}
datasource_type={this.props.datasource_type}
onQuery={this.onQuery.bind(this, this.props.form_data)}
onQuery={this.onQuery.bind(this)}
/>
</div>
<div className="col-sm-8">
@ -147,10 +153,10 @@ ExploreViewContainer.propTypes = propTypes;
function mapStateToProps(state) {
return {
datasource_type: state.datasource_type,
form_data: state.viz.form_data,
chartStatus: state.chartStatus,
datasource_type: state.datasource_type,
fields: state.fields,
form_data: state.viz.form_data,
};
}

View File

@ -32,6 +32,7 @@ const bootstrappedState = Object.assign(
chartUpdateStartTime: now(),
chartUpdateEndTime: null,
chartStatus: 'loading',
queryResponse: null,
}
);
bootstrappedState.viz.form_data.datasource = parseInt(bootstrapData.datasource_id, 10);

View File

@ -2,7 +2,6 @@
import { defaultFormData } from '../stores/store';
import * as actions from '../actions/exploreActions';
import { now } from '../../modules/dates';
import { getExploreUrl } from '../exploreUtils';
export const exploreReducer = function (state, action) {
const actionHandlers = {
@ -70,34 +69,30 @@ export const exploreReducer = function (state, action) {
state,
{
chartStatus: 'success',
viz: Object.assign({}, state.viz, { query: action.query }),
queryResponse: action.queryResponse,
}
);
},
[actions.CHART_UPDATE_STARTED]() {
const chartUpdateStartTime = now();
const form_data = Object.assign({}, state.viz.form_data);
const datasource_type = state.datasource_type;
const vizUpdates = {
json_endpoint: getExploreUrl(form_data, datasource_type, 'json'),
csv_endpoint: getExploreUrl(form_data, datasource_type, 'csv'),
standalone_endpoint:
getExploreUrl(form_data, datasource_type, 'standalone'),
};
return Object.assign({}, state,
{
chartStatus: 'loading',
chartUpdateEndTime: null,
chartUpdateStartTime,
viz: Object.assign({}, state.viz, vizUpdates),
chartUpdateStartTime: now(),
});
},
[actions.CHART_RENDERING_FAILED]() {
return Object.assign({}, state, {
chartStatus: 'failed',
chartAlert: 'An error occurred while rendering the visualization: ' + action.error,
});
},
[actions.CHART_UPDATE_FAILED]() {
return Object.assign({}, state, {
chartStatus: 'failed',
chartAlert: action.error,
chartAlert: action.queryResponse.error,
chartUpdateEndTime: now(),
viz: Object.assign({}, state.viz, { query: action.query }),
queryResponse: action.queryResponse,
});
},
[actions.UPDATE_CHART_STATUS]() {
@ -108,7 +103,10 @@ export const exploreReducer = function (state, action) {
return newState;
},
[actions.REMOVE_CHART_ALERT]() {
return Object.assign({}, state, { chartAlert: null });
if (state.chartAlert !== null) {
return Object.assign({}, state, { chartAlert: null });
}
return state;
},
[actions.SAVE_SLICE_FAILED]() {
return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' });

View File

@ -222,14 +222,19 @@ const px = function () {
timer = setInterval(stopwatch, 10);
$('#timer').removeClass('label-danger label-success');
$('#timer').addClass('label-warning');
this.viz.render();
$.getJSON(this.jsonEndpoint(), queryResponse => {
try {
vizMap[data.form_data.viz_type](this, queryResponse);
this.done(queryResponse);
} catch (e) {
this.error('An error occurred while rendering the visualization: ' + e);
}
}).fail(err => {
this.error(err.responseText, err);
});
},
resize() {
token.find('img.loading').show();
container.fadeTo(0.5, 0.25);
container.css('height', this.height());
this.viz.render();
this.viz.resize();
this.render();
},
addFilter(col, vals) {
controller.addFilter(sliceId, col, vals);
@ -247,7 +252,6 @@ const px = function () {
controller.removeFilter(sliceId, col, vals);
},
};
slice.viz = vizMap[data.form_data.viz_type](slice);
return slice;
};
// Export public functions

View File

@ -3,197 +3,183 @@ import { formatDate } from '../javascripts/modules/dates';
require('./big_number.css');
function bigNumberVis(slice) {
function render() {
const div = d3.select(slice.selector);
d3.json(slice.jsonEndpoint(), function (error, payload) {
// Define the percentage bounds that define color from red to green
if (error !== null) {
slice.error(error.responseText, error);
return;
}
div.html(''); // reset
function bigNumberVis(slice, payload) {
const div = d3.select(slice.selector);
// Define the percentage bounds that define color from red to green
div.html(''); // reset
const fd = payload.form_data;
const json = payload.data;
const fd = payload.form_data;
const json = payload.data;
const f = d3.format(fd.y_axis_format);
const fp = d3.format('+.1%');
const width = slice.width();
const height = slice.height();
const svg = div.append('svg');
svg.attr('width', width);
svg.attr('height', height);
const data = json.data;
let vCompare;
let v;
if (fd.viz_type === 'big_number') {
v = data[data.length - 1][1];
const f = d3.format(fd.y_axis_format);
const fp = d3.format('+.1%');
const width = slice.width();
const height = slice.height();
const svg = div.append('svg');
svg.attr('width', width);
svg.attr('height', height);
const data = json.data;
let vCompare;
let v;
if (fd.viz_type === 'big_number') {
v = data[data.length - 1][1];
} else {
v = data[0][0];
}
if (json.compare_lag > 0) {
const pos = data.length - (json.compare_lag + 1);
if (pos >= 0) {
const vAnchor = data[pos][1];
if (vAnchor !== 0) {
vCompare = (v - vAnchor) / Math.abs(vAnchor);
} else {
v = data[0][0];
vCompare = 0;
}
if (json.compare_lag > 0) {
const pos = data.length - (json.compare_lag + 1);
if (pos >= 0) {
const vAnchor = data[pos][1];
if (vAnchor !== 0) {
vCompare = (v - vAnchor) / Math.abs(vAnchor);
} else {
vCompare = 0;
}
}
}
const dateExt = d3.extent(data, (d) => d[0]);
const valueExt = d3.extent(data, (d) => d[1]);
}
}
const dateExt = d3.extent(data, (d) => d[0]);
const valueExt = d3.extent(data, (d) => d[1]);
const margin = 20;
const scaleX = d3.time.scale.utc().domain(dateExt).range([margin, width - margin]);
const scaleY = d3.scale.linear().domain(valueExt).range([height - (margin), margin]);
const colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
const scaleColor = d3.scale
.linear().domain([-1, 1])
.interpolate(d3.interpolateHsl)
.range(colorRange)
.clamp(true);
const line = d3.svg.line()
.x(function (d) {
return scaleX(d[0]);
})
.y(function (d) {
return scaleY(d[1]);
})
.interpolate('basis');
const margin = 20;
const scaleX = d3.time.scale.utc().domain(dateExt).range([margin, width - margin]);
const scaleY = d3.scale.linear().domain(valueExt).range([height - (margin), margin]);
const colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
const scaleColor = d3.scale
.linear().domain([-1, 1])
.interpolate(d3.interpolateHsl)
.range(colorRange)
.clamp(true);
const line = d3.svg.line()
.x(function (d) {
return scaleX(d[0]);
})
.y(function (d) {
return scaleY(d[1]);
})
.interpolate('basis');
let y = height / 2;
let g = svg.append('g');
// Printing big number
g.append('g').attr('class', 'digits')
.attr('opacity', 1)
.append('text')
.attr('x', width / 2)
.attr('y', y)
.attr('class', 'big')
.attr('alignment-baseline', 'middle')
.attr('id', 'bigNumber')
.style('font-weight', 'bold')
.style('cursor', 'pointer')
.text(f(v))
.style('font-size', d3.min([height, width]) / 3.5)
.style('text-anchor', 'middle')
.attr('fill', 'black');
let y = height / 2;
let g = svg.append('g');
// Printing big number
g.append('g').attr('class', 'digits')
.attr('opacity', 1)
.append('text')
.attr('x', width / 2)
.attr('y', y)
.attr('class', 'big')
.attr('alignment-baseline', 'middle')
.attr('id', 'bigNumber')
.style('font-weight', 'bold')
.style('cursor', 'pointer')
.text(f(v))
.style('font-size', d3.min([height, width]) / 3.5)
.style('text-anchor', 'middle')
.attr('fill', 'black');
// Printing big number subheader text
if (json.subheader !== null) {
g.append('text')
.attr('x', width / 2)
.attr('y', (height / 16) * 12)
.text(json.subheader)
.attr('id', 'subheader_text')
.style('font-size', d3.min([height, width]) / 8)
.style('text-anchor', 'middle');
}
if (fd.viz_type === 'big_number') {
// Drawing trend line
g.append('path')
.attr('d', function () {
return line(data);
})
.attr('stroke-width', 5)
.attr('opacity', 0.5)
.attr('fill', 'none')
.attr('stroke-linecap', 'round')
.attr('stroke', 'grey');
g = svg.append('g')
.attr('class', 'digits')
.attr('opacity', 1);
if (vCompare !== null) {
y = (height / 8) * 3;
}
const c = scaleColor(vCompare);
// Printing compare %
if (vCompare) {
g.append('text')
.attr('x', width / 2)
.attr('y', (height / 16) * 12)
.text(fp(vCompare) + json.compare_suffix)
.style('font-size', d3.min([height, width]) / 8)
.style('text-anchor', 'middle')
.attr('fill', c)
.attr('stroke', c);
}
const gAxis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
g = gAxis.append('g');
const xAxis = d3.svg.axis()
.scale(scaleX)
.orient('bottom')
.ticks(4)
.tickFormat(formatDate);
g.call(xAxis);
g.attr('transform', 'translate(0,' + (height - margin) + ')');
g = gAxis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
const yAxis = d3.svg.axis()
.scale(scaleY)
.orient('left')
.tickFormat(d3.format(fd.y_axis_format))
.tickValues(valueExt);
g.call(yAxis);
g.selectAll('text')
.style('text-anchor', 'end')
.attr('y', '-7')
.attr('x', '-4');
g.selectAll('text')
.style('font-size', '10px');
div.on('mouseover', function () {
const el = d3.select(this);
el.selectAll('path')
.transition()
.duration(500)
.attr('opacity', 1)
.style('stroke-width', '2px');
el.selectAll('g.digits')
.transition()
.duration(500)
.attr('opacity', 0.1);
el.selectAll('g.axis')
.transition()
.duration(500)
.attr('opacity', 1);
})
.on('mouseout', function () {
const el = d3.select(this);
el.select('path')
.transition()
.duration(500)
.attr('opacity', 0.5)
.style('stroke-width', '5px');
el.selectAll('g.digits')
.transition()
.duration(500)
.attr('opacity', 1);
el.selectAll('g.axis')
.transition()
.duration(500)
.attr('opacity', 0);
});
}
slice.done(payload);
});
// Printing big number subheader text
if (json.subheader !== null) {
g.append('text')
.attr('x', width / 2)
.attr('y', (height / 16) * 12)
.text(json.subheader)
.attr('id', 'subheader_text')
.style('font-size', d3.min([height, width]) / 8)
.style('text-anchor', 'middle');
}
return {
render,
resize: render,
};
if (fd.viz_type === 'big_number') {
// Drawing trend line
g.append('path')
.attr('d', function () {
return line(data);
})
.attr('stroke-width', 5)
.attr('opacity', 0.5)
.attr('fill', 'none')
.attr('stroke-linecap', 'round')
.attr('stroke', 'grey');
g = svg.append('g')
.attr('class', 'digits')
.attr('opacity', 1);
if (vCompare !== null) {
y = (height / 8) * 3;
}
const c = scaleColor(vCompare);
// Printing compare %
if (vCompare) {
g.append('text')
.attr('x', width / 2)
.attr('y', (height / 16) * 12)
.text(fp(vCompare) + json.compare_suffix)
.style('font-size', d3.min([height, width]) / 8)
.style('text-anchor', 'middle')
.attr('fill', c)
.attr('stroke', c);
}
const gAxis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
g = gAxis.append('g');
const xAxis = d3.svg.axis()
.scale(scaleX)
.orient('bottom')
.ticks(4)
.tickFormat(formatDate);
g.call(xAxis);
g.attr('transform', 'translate(0,' + (height - margin) + ')');
g = gAxis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
const yAxis = d3.svg.axis()
.scale(scaleY)
.orient('left')
.tickFormat(d3.format(fd.y_axis_format))
.tickValues(valueExt);
g.call(yAxis);
g.selectAll('text')
.style('text-anchor', 'end')
.attr('y', '-7')
.attr('x', '-4');
g.selectAll('text')
.style('font-size', '10px');
div.on('mouseover', function () {
const el = d3.select(this);
el.selectAll('path')
.transition()
.duration(500)
.attr('opacity', 1)
.style('stroke-width', '2px');
el.selectAll('g.digits')
.transition()
.duration(500)
.attr('opacity', 0.1);
el.selectAll('g.axis')
.transition()
.duration(500)
.attr('opacity', 1);
})
.on('mouseout', function () {
const el = d3.select(this);
el.select('path')
.transition()
.duration(500)
.attr('opacity', 0.5)
.style('stroke-width', '5px');
el.selectAll('g.digits')
.transition()
.duration(500)
.attr('opacity', 1);
el.selectAll('g.axis')
.transition()
.duration(500)
.attr('opacity', 0);
});
}
}
module.exports = bigNumberVis;

View File

@ -7,47 +7,32 @@ require('../node_modules/cal-heatmap/cal-heatmap.css');
const CalHeatMap = require('cal-heatmap');
function calHeatmap(slice) {
const render = function () {
const div = d3.select(slice.selector);
d3.json(slice.jsonEndpoint(), function (error, json) {
const data = json.data;
if (error !== null) {
slice.error(error.responseText, error);
return;
}
function calHeatmap(slice, payload) {
const div = d3.select(slice.selector);
const data = payload.data;
div.selectAll('*').remove();
const cal = new CalHeatMap();
div.selectAll('*').remove();
const cal = new CalHeatMap();
const timestamps = data.timestamps;
const extents = d3.extent(Object.keys(timestamps), (key) => timestamps[key]);
const step = (extents[1] - extents[0]) / 5;
const timestamps = data.timestamps;
const extents = d3.extent(Object.keys(timestamps), (key) => timestamps[key]);
const step = (extents[1] - extents[0]) / 5;
try {
cal.init({
start: data.start,
data: timestamps,
itemSelector: slice.selector,
tooltip: true,
domain: data.domain,
subDomain: data.subdomain,
range: data.range,
browsing: true,
legend: [extents[0], extents[0] + step, extents[0] + step * 2, extents[0] + step * 3],
});
} catch (e) {
slice.error(e);
}
slice.done(json);
try {
cal.init({
start: data.start,
data: timestamps,
itemSelector: slice.selector,
tooltip: true,
domain: data.domain,
subDomain: data.subdomain,
range: data.range,
browsing: true,
legend: [extents[0], extents[0] + step, extents[0] + step * 2, extents[0] + step * 3],
});
};
return {
render,
resize: render,
};
} catch (e) {
slice.error(e);
}
}
module.exports = calHeatmap;

View File

@ -4,179 +4,164 @@ import d3 from 'd3';
require('./directed_force.css');
/* Modified from http://bl.ocks.org/d3noob/5141278 */
function directedForceVis(slice) {
const render = function () {
const div = d3.select(slice.selector);
const width = slice.width();
const height = slice.height() - 25;
d3.json(slice.jsonEndpoint(), function (error, json) {
if (error !== null) {
slice.error(error.responseText, error);
return;
}
const linkLength = json.form_data.link_length || 200;
const charge = json.form_data.charge || -500;
const directedForceVis = function (slice, json) {
const div = d3.select(slice.selector);
const width = slice.width();
const height = slice.height() - 25;
const linkLength = json.form_data.link_length || 200;
const charge = json.form_data.charge || -500;
const links = json.data;
const nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function (link) {
link.source = nodes[link.source] || (nodes[link.source] = {
name: link.source,
});
link.target = nodes[link.target] || (nodes[link.target] = {
name: link.target,
});
link.value = Number(link.value);
const targetName = link.target.name;
const sourceName = link.source.name;
if (nodes[targetName].total === undefined) {
nodes[targetName].total = link.value;
}
if (nodes[sourceName].total === undefined) {
nodes[sourceName].total = 0;
}
if (nodes[targetName].max === undefined) {
nodes[targetName].max = 0;
}
if (link.value > nodes[targetName].max) {
nodes[targetName].max = link.value;
}
if (nodes[targetName].min === undefined) {
nodes[targetName].min = 0;
}
if (link.value > nodes[targetName].min) {
nodes[targetName].min = link.value;
}
nodes[targetName].total += link.value;
});
/* eslint-disable no-use-before-define */
// add the curvy lines
function tick() {
path.attr('d', function (d) {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const dr = Math.sqrt(dx * dx + dy * dy);
return (
'M' +
d.source.x + ',' +
d.source.y + 'A' +
dr + ',' + dr + ' 0 0,1 ' +
d.target.x + ',' +
d.target.y
);
});
node.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
/* eslint-enable no-use-before-define */
const force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(linkLength)
.charge(charge)
.on('tick', tick)
.start();
div.selectAll('*').remove();
const svg = div.append('svg')
.attr('width', width)
.attr('height', height);
// build the arrow.
svg.append('svg:defs').selectAll('marker')
.data(['end']) // Different link/path types can be defined here
.enter()
.append('svg:marker') // This section adds in the arrows
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', -1.5)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
const edgeScale = d3.scale.linear()
.range([0.1, 0.5]);
// add the links and the arrows
const path = svg.append('svg:g').selectAll('path')
.data(force.links())
.enter()
.append('svg:path')
.attr('class', 'link')
.style('opacity', function (d) {
return edgeScale(d.value / d.target.max);
})
.attr('marker-end', 'url(#end)');
// define the nodes
const node = svg.selectAll('.node')
.data(force.nodes())
.enter()
.append('g')
.attr('class', 'node')
.on('mouseenter', function () {
d3.select(this)
.select('circle')
.transition()
.style('stroke-width', 5);
d3.select(this)
.select('text')
.transition()
.style('font-size', 25);
})
.on('mouseleave', function () {
d3.select(this)
.select('circle')
.transition()
.style('stroke-width', 1.5);
d3.select(this)
.select('text')
.transition()
.style('font-size', 12);
})
.call(force.drag);
// add the nodes
const ext = d3.extent(d3.values(nodes), function (d) {
return Math.sqrt(d.total);
});
const circleScale = d3.scale.linear()
.domain(ext)
.range([3, 30]);
node.append('circle')
.attr('r', function (d) {
return circleScale(Math.sqrt(d.total));
});
// add the text
node.append('text')
.attr('x', 6)
.attr('dy', '.35em')
.text(function (d) {
return d.name;
});
slice.done(json);
const links = json.data;
const nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function (link) {
link.source = nodes[link.source] || (nodes[link.source] = {
name: link.source,
});
};
return {
render,
resize: render,
};
}
link.target = nodes[link.target] || (nodes[link.target] = {
name: link.target,
});
link.value = Number(link.value);
const targetName = link.target.name;
const sourceName = link.source.name;
if (nodes[targetName].total === undefined) {
nodes[targetName].total = link.value;
}
if (nodes[sourceName].total === undefined) {
nodes[sourceName].total = 0;
}
if (nodes[targetName].max === undefined) {
nodes[targetName].max = 0;
}
if (link.value > nodes[targetName].max) {
nodes[targetName].max = link.value;
}
if (nodes[targetName].min === undefined) {
nodes[targetName].min = 0;
}
if (link.value > nodes[targetName].min) {
nodes[targetName].min = link.value;
}
nodes[targetName].total += link.value;
});
/* eslint-disable no-use-before-define */
// add the curvy lines
function tick() {
path.attr('d', function (d) {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const dr = Math.sqrt(dx * dx + dy * dy);
return (
'M' +
d.source.x + ',' +
d.source.y + 'A' +
dr + ',' + dr + ' 0 0,1 ' +
d.target.x + ',' +
d.target.y
);
});
node.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
/* eslint-enable no-use-before-define */
const force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(linkLength)
.charge(charge)
.on('tick', tick)
.start();
div.selectAll('*').remove();
const svg = div.append('svg')
.attr('width', width)
.attr('height', height);
// build the arrow.
svg.append('svg:defs').selectAll('marker')
.data(['end']) // Different link/path types can be defined here
.enter()
.append('svg:marker') // This section adds in the arrows
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', -1.5)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
const edgeScale = d3.scale.linear()
.range([0.1, 0.5]);
// add the links and the arrows
const path = svg.append('svg:g').selectAll('path')
.data(force.links())
.enter()
.append('svg:path')
.attr('class', 'link')
.style('opacity', function (d) {
return edgeScale(d.value / d.target.max);
})
.attr('marker-end', 'url(#end)');
// define the nodes
const node = svg.selectAll('.node')
.data(force.nodes())
.enter()
.append('g')
.attr('class', 'node')
.on('mouseenter', function () {
d3.select(this)
.select('circle')
.transition()
.style('stroke-width', 5);
d3.select(this)
.select('text')
.transition()
.style('font-size', 25);
})
.on('mouseleave', function () {
d3.select(this)
.select('circle')
.transition()
.style('stroke-width', 1.5);
d3.select(this)
.select('text')
.transition()
.style('font-size', 12);
})
.call(force.drag);
// add the nodes
const ext = d3.extent(d3.values(nodes), function (d) {
return Math.sqrt(d.total);
});
const circleScale = d3.scale.linear()
.domain(ext)
.range([3, 30]);
node.append('circle')
.attr('r', function (d) {
return circleScale(Math.sqrt(d.total));
});
// add the text
node.append('text')
.attr('x', 6)
.attr('dy', '.35em')
.text(function (d) {
return d.name;
});
};
module.exports = directedForceVis;

View File

@ -1,5 +1,4 @@
// JS
const $ = require('jquery');
import d3 from 'd3';
import React from 'react';
@ -109,40 +108,29 @@ class FilterBox extends React.Component {
FilterBox.propTypes = propTypes;
FilterBox.defaultProps = defaultProps;
function filterBox(slice) {
const refresh = function () {
const d3token = d3.select(slice.selector);
d3token.selectAll('*').remove();
function filterBox(slice, payload) {
const d3token = d3.select(slice.selector);
d3token.selectAll('*').remove();
// filter box should ignore the dashboard's filters
const url = slice.jsonEndpoint({ extraFilters: false });
$.getJSON(url, (payload) => {
const fd = payload.form_data;
const filtersChoices = {};
// Making sure the ordering of the fields matches the setting in the
// dropdown as it may have been shuffled while serialized to json
payload.form_data.groupby.forEach((f) => {
filtersChoices[f] = payload.data[f];
});
ReactDOM.render(
<FilterBox
filtersChoices={filtersChoices}
onChange={slice.setFilter}
showDateFilter={fd.date_filter}
origSelectedValues={slice.getFilters() || {}}
/>,
document.getElementById(slice.containerId)
);
slice.done(payload);
})
.fail(function (xhr) {
slice.error(xhr.responseText, xhr);
});
};
return {
render: refresh,
resize: () => {},
};
// filter box should ignore the dashboard's filters
// TODO FUCK
// const url = slice.jsonEndpoint({ extraFilters: false });
const fd = payload.form_data;
const filtersChoices = {};
// Making sure the ordering of the fields matches the setting in the
// dropdown as it may have been shuffled while serialized to json
payload.form_data.groupby.forEach((f) => {
filtersChoices[f] = payload.data[f];
});
ReactDOM.render(
<FilterBox
filtersChoices={filtersChoices}
onChange={slice.setFilter}
showDateFilter={fd.date_filter}
origSelectedValues={slice.getFilters() || {}}
/>,
document.getElementById(slice.containerId)
);
}
module.exports = filterBox;

View File

@ -8,225 +8,210 @@ require('./heatmap.css');
// Inspired from http://bl.ocks.org/mbostock/3074470
// https://jsfiddle.net/cyril123/h0reyumq/
function heatmapVis(slice) {
function refresh() {
// Header for panel in explore v2
const header = document.getElementById('slice-header');
const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
const margin = {
top: headerHeight,
right: 10,
bottom: 35,
left: 35,
};
d3.json(slice.jsonEndpoint(), function (error, payload) {
if (error) {
slice.error(error.responseText, error);
return;
}
const data = payload.data;
// Dynamically adjusts based on max x / y category lengths
function adjustMargins() {
const pixelsPerCharX = 4.5; // approx, depends on font size
const pixelsPerCharY = 6.8; // approx, depends on font size
let longestX = 1;
let longestY = 1;
let datum;
for (let i = 0; i < data.length; i++) {
datum = data[i];
longestX = Math.max(longestX, datum.x.length || 1);
longestY = Math.max(longestY, datum.y.length || 1);
}
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
}
function ordScale(k, rangeBands, reverse = false) {
let domain = {};
$.each(data, function (i, d) {
domain[d[k]] = true;
});
domain = Object.keys(domain).sort(function (a, b) {
return b - a;
});
if (reverse) {
domain.reverse();
}
if (rangeBands === undefined) {
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
}
return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
}
slice.container.html('');
const matrix = {};
const fd = payload.form_data;
adjustMargins();
const width = slice.width();
const height = slice.height();
const hmWidth = width - (margin.left + margin.right);
const hmHeight = height - (margin.bottom + margin.top);
const fp = d3.format('.3p');
const xScale = ordScale('x');
const yScale = ordScale('y', undefined, true);
const xRbScale = ordScale('x', [0, hmWidth]);
const yRbScale = ordScale('y', [hmHeight, 0]);
const X = 0;
const Y = 1;
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
const color = colorScalerFactory(fd.linear_color_scheme);
const scale = [
d3.scale.linear()
.domain([0, heatmapDim[X]])
.range([0, hmWidth]),
d3.scale.linear()
.domain([0, heatmapDim[Y]])
.range([0, hmHeight]),
];
const container = d3.select(slice.selector);
const canvas = container.append('canvas')
.attr('width', heatmapDim[X])
.attr('height', heatmapDim[Y])
.style('width', hmWidth + 'px')
.style('height', hmHeight + 'px')
.style('image-rendering', fd.canvas_image_rendering)
.style('left', margin.left + 'px')
.style('top', margin.top + headerHeight + 'px')
.style('position', 'absolute');
const svg = container.append('svg')
.attr('width', width)
.attr('height', height)
.style('left', '0px')
.style('top', headerHeight + 'px')
.style('position', 'absolute');
const rect = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.append('rect')
.style('fill-opacity', 0)
.attr('stroke', 'black')
.attr('width', hmWidth)
.attr('height', hmHeight);
const tip = d3.tip()
.attr('class', 'd3-tip')
.offset(function () {
const k = d3.mouse(this);
const x = k[0] - (hmWidth / 2);
return [k[1] - 20, x];
})
.html(function () {
let s = '';
const k = d3.mouse(this);
const m = Math.floor(scale[0].invert(k[0]));
const n = Math.floor(scale[1].invert(k[1]));
if (m in matrix && n in matrix[m]) {
const obj = matrix[m][n];
s += '<div><b>' + fd.all_columns_x + ': </b>' + obj.x + '<div>';
s += '<div><b>' + fd.all_columns_y + ': </b>' + obj.y + '<div>';
s += '<div><b>' + fd.metric + ': </b>' + obj.v + '<div>';
s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
tip.style('display', null);
} else {
// this is a hack to hide the tooltip because we have map it to a single <rect>
// d3-tip toggles opacity and calling hide here is undone by the lib after this call
tip.style('display', 'none');
}
return s;
});
rect.call(tip);
const xAxis = d3.svg.axis()
.scale(xRbScale)
.tickValues(xRbScale.domain().filter(
function (d, i) {
return !(i % (parseInt(fd.xscale_interval, 10)));
}))
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(yRbScale)
.tickValues(yRbScale.domain().filter(
function (d, i) {
return !(i % (parseInt(fd.yscale_interval, 10)));
}))
.orient('left');
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('transform', 'rotate(-45)');
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(yAxis);
rect.on('mousemove', tip.show);
rect.on('mouseout', tip.hide);
const context = canvas.node().getContext('2d');
context.imageSmoothingEnabled = false;
// Compute the pixel colors; scaled by CSS.
function createImageObj() {
const imageObj = new Image();
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
const pixs = {};
$.each(data, function (i, d) {
const c = d3.rgb(color(d.perc));
const x = xScale(d.x);
const y = yScale(d.y);
pixs[x + (y * xScale.domain().length)] = c;
if (matrix[x] === undefined) {
matrix[x] = {};
}
if (matrix[x][y] === undefined) {
matrix[x][y] = d;
}
});
let p = -1;
for (let i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
let c = pixs[i];
let alpha = 255;
if (c === undefined) {
c = d3.rgb('#F00');
alpha = 0;
}
image.data[++p] = c.r;
image.data[++p] = c.g;
image.data[++p] = c.b;
image.data[++p] = alpha;
}
context.putImageData(image, 0, 0);
imageObj.src = canvas.node().toDataURL();
}
createImageObj();
slice.done(payload);
});
}
return {
render: refresh,
resize: refresh,
function heatmapVis(slice, payload) {
// Header for panel in explore v2
const header = document.getElementById('slice-header');
const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
const margin = {
top: headerHeight,
right: 10,
bottom: 35,
left: 35,
};
const data = payload.data;
// Dynamically adjusts based on max x / y category lengths
function adjustMargins() {
const pixelsPerCharX = 4.5; // approx, depends on font size
const pixelsPerCharY = 6.8; // approx, depends on font size
let longestX = 1;
let longestY = 1;
let datum;
for (let i = 0; i < data.length; i++) {
datum = data[i];
longestX = Math.max(longestX, datum.x.length || 1);
longestY = Math.max(longestY, datum.y.length || 1);
}
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
}
function ordScale(k, rangeBands, reverse = false) {
let domain = {};
$.each(data, function (i, d) {
domain[d[k]] = true;
});
domain = Object.keys(domain).sort(function (a, b) {
return b - a;
});
if (reverse) {
domain.reverse();
}
if (rangeBands === undefined) {
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
}
return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
}
slice.container.html('');
const matrix = {};
const fd = payload.form_data;
adjustMargins();
const width = slice.width();
const height = slice.height();
const hmWidth = width - (margin.left + margin.right);
const hmHeight = height - (margin.bottom + margin.top);
const fp = d3.format('.3p');
const xScale = ordScale('x');
const yScale = ordScale('y', undefined, true);
const xRbScale = ordScale('x', [0, hmWidth]);
const yRbScale = ordScale('y', [hmHeight, 0]);
const X = 0;
const Y = 1;
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
const color = colorScalerFactory(fd.linear_color_scheme);
const scale = [
d3.scale.linear()
.domain([0, heatmapDim[X]])
.range([0, hmWidth]),
d3.scale.linear()
.domain([0, heatmapDim[Y]])
.range([0, hmHeight]),
];
const container = d3.select(slice.selector);
const canvas = container.append('canvas')
.attr('width', heatmapDim[X])
.attr('height', heatmapDim[Y])
.style('width', hmWidth + 'px')
.style('height', hmHeight + 'px')
.style('image-rendering', fd.canvas_image_rendering)
.style('left', margin.left + 'px')
.style('top', margin.top + headerHeight + 'px')
.style('position', 'absolute');
const svg = container.append('svg')
.attr('width', width)
.attr('height', height)
.style('left', '0px')
.style('top', headerHeight + 'px')
.style('position', 'absolute');
const rect = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.append('rect')
.style('fill-opacity', 0)
.attr('stroke', 'black')
.attr('width', hmWidth)
.attr('height', hmHeight);
const tip = d3.tip()
.attr('class', 'd3-tip')
.offset(function () {
const k = d3.mouse(this);
const x = k[0] - (hmWidth / 2);
return [k[1] - 20, x];
})
.html(function () {
let s = '';
const k = d3.mouse(this);
const m = Math.floor(scale[0].invert(k[0]));
const n = Math.floor(scale[1].invert(k[1]));
if (m in matrix && n in matrix[m]) {
const obj = matrix[m][n];
s += '<div><b>' + fd.all_columns_x + ': </b>' + obj.x + '<div>';
s += '<div><b>' + fd.all_columns_y + ': </b>' + obj.y + '<div>';
s += '<div><b>' + fd.metric + ': </b>' + obj.v + '<div>';
s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
tip.style('display', null);
} else {
// this is a hack to hide the tooltip because we have map it to a single <rect>
// d3-tip toggles opacity and calling hide here is undone by the lib after this call
tip.style('display', 'none');
}
return s;
});
rect.call(tip);
const xAxis = d3.svg.axis()
.scale(xRbScale)
.tickValues(xRbScale.domain().filter(
function (d, i) {
return !(i % (parseInt(fd.xscale_interval, 10)));
}))
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(yRbScale)
.tickValues(yRbScale.domain().filter(
function (d, i) {
return !(i % (parseInt(fd.yscale_interval, 10)));
}))
.orient('left');
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('transform', 'rotate(-45)');
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(yAxis);
rect.on('mousemove', tip.show);
rect.on('mouseout', tip.hide);
const context = canvas.node().getContext('2d');
context.imageSmoothingEnabled = false;
// Compute the pixel colors; scaled by CSS.
function createImageObj() {
const imageObj = new Image();
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
const pixs = {};
$.each(data, function (i, d) {
const c = d3.rgb(color(d.perc));
const x = xScale(d.x);
const y = yScale(d.y);
pixs[x + (y * xScale.domain().length)] = c;
if (matrix[x] === undefined) {
matrix[x] = {};
}
if (matrix[x][y] === undefined) {
matrix[x][y] = d;
}
});
let p = -1;
for (let i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
let c = pixs[i];
let alpha = 255;
if (c === undefined) {
c = d3.rgb('#F00');
alpha = 0;
}
image.data[++p] = c.r;
image.data[++p] = c.g;
image.data[++p] = c.b;
image.data[++p] = alpha;
}
context.putImageData(image, 0, 0);
imageObj.src = canvas.node().toDataURL();
}
createImageObj();
}
module.exports = heatmapVis;

View File

@ -3,9 +3,8 @@ import d3 from 'd3';
require('./histogram.css');
function histogram(slice) {
let div;
function histogram(slice, payload) {
const div = d3.select(slice.selector);
const draw = function (data, numBins) {
// Set Margins
const margin = {
@ -127,26 +126,9 @@ function histogram(slice) {
.classed('minor', true);
};
const render = function () {
div = d3.select(slice.selector);
d3.json(slice.jsonEndpoint(), function (error, json) {
if (error !== null) {
slice.error(error.responseText, error);
return;
}
const numBins = Number(json.form_data.link_length) || 10;
div.selectAll('*').remove();
draw(json.data, numBins);
slice.done(json);
});
};
return {
render,
resize: render,
};
const numBins = Number(payload.form_data.link_length) || 10;
div.selectAll('*').remove();
draw(payload.data, numBins);
}
module.exports = histogram;

View File

@ -190,53 +190,38 @@ const horizonChart = function () {
return my;
};
function horizonViz(slice) {
function refresh() {
d3.json(slice.jsonEndpoint(), function (error, payload) {
const fd = payload.form_data;
if (error) {
slice.error(error.responseText, error);
return;
}
const div = d3.select(slice.selector);
div.selectAll('*').remove();
let extent;
if (fd.horizon_color_scale === 'overall') {
let allValues = [];
payload.data.forEach(function (d) {
allValues = allValues.concat(d.values);
});
extent = d3.extent(allValues, (d) => d.y);
} else if (fd.horizon_color_scale === 'change') {
payload.data.forEach(function (series) {
const t0y = series.values[0].y; // value at time 0
series.values = series.values.map((d) =>
Object.assign({}, d, { y: d.y - t0y })
);
});
}
div.selectAll('.horizon')
.data(payload.data)
.enter()
.append('div')
.attr('class', 'horizon')
.each(function (d, i) {
horizonChart()
.height(fd.series_height)
.width(slice.width())
.extent(extent)
.title(d.key)
.call(this, d.values, i);
});
slice.done(payload);
function horizonViz(slice, payload) {
const fd = payload.form_data;
const div = d3.select(slice.selector);
div.selectAll('*').remove();
let extent;
if (fd.horizon_color_scale === 'overall') {
let allValues = [];
payload.data.forEach(function (d) {
allValues = allValues.concat(d.values);
});
extent = d3.extent(allValues, (d) => d.y);
} else if (fd.horizon_color_scale === 'change') {
payload.data.forEach(function (series) {
const t0y = series.values[0].y; // value at time 0
series.values = series.values.map((d) =>
Object.assign({}, d, { y: d.y - t0y })
);
});
}
return {
render: refresh,
resize: refresh,
};
div.selectAll('.horizon')
.data(payload.data)
.enter()
.append('div')
.attr('class', 'horizon')
.each(function (d, i) {
horizonChart()
.height(fd.series_height)
.width(slice.width())
.extent(extent)
.title(d.key)
.call(this, d.values, i);
});
}
module.exports = horizonViz;

View File

@ -1,25 +1,12 @@
const $ = require('jquery');
function iframeWidget(slice) {
function refresh() {
$('#code').attr('rows', '15');
$.getJSON(slice.jsonEndpoint(), function (payload) {
const url = slice.render_template(payload.form_data.url);
slice.container.html('<iframe style="width:100%;"></iframe>');
const iframe = slice.container.find('iframe');
iframe.css('height', slice.height());
iframe.attr('src', url);
slice.done(payload);
})
.fail(function (xhr) {
slice.error(xhr.responseText, xhr);
});
}
return {
render: refresh,
resize: refresh,
};
function iframeWidget(slice, payload) {
$('#code').attr('rows', '15');
const url = slice.render_template(payload.form_data.url);
slice.container.html('<iframe style="width:100%;"></iframe>');
const iframe = slice.container.find('iframe');
iframe.css('height', slice.height());
iframe.attr('src', url);
}
module.exports = iframeWidget;

View File

@ -273,84 +273,68 @@ MapboxViz.propTypes = {
viewportZoom: React.PropTypes.number,
};
function mapbox(slice) {
function mapbox(slice, json) {
const div = d3.select(slice.selector);
const DEFAULT_POINT_RADIUS = 60;
const DEFAULT_MAX_ZOOM = 16;
let clusterer;
const render = function () {
const div = d3.select(slice.selector);
d3.json(slice.jsonEndpoint(), function (error, json) {
if (error !== null) {
slice.error(error.responseText);
return;
// Validate mapbox color
const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(json.data.color);
if (rgb === null) {
slice.error('Color field must be of form \'rgb(%d, %d, %d)\'');
return;
}
const aggName = json.data.aggregatorName;
let reducer;
if (aggName === 'sum' || !json.data.customMetric) {
reducer = function (a, b) {
return a + b;
};
} else if (aggName === 'min') {
reducer = Math.min;
} else if (aggName === 'max') {
reducer = Math.max;
} else {
reducer = function (a, b) {
if (a instanceof Array) {
if (b instanceof Array) {
return a.concat(b);
}
a.push(b);
return a;
}
// Validate mapbox color
const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(json.data.color);
if (rgb === null) {
slice.error('Color field must be of form \'rgb(%d, %d, %d)\'');
return;
if (b instanceof Array) {
b.push(a);
return b;
}
return [a, b];
};
}
const aggName = json.data.aggregatorName;
let reducer;
clusterer = supercluster({
radius: json.data.clusteringRadius,
maxZoom: DEFAULT_MAX_ZOOM,
metricKey: 'metric',
metricReducer: reducer,
});
clusterer.load(json.data.geoJSON.features);
if (aggName === 'sum' || !json.data.customMetric) {
reducer = function (a, b) {
return a + b;
};
} else if (aggName === 'min') {
reducer = Math.min;
} else if (aggName === 'max') {
reducer = Math.max;
} else {
reducer = function (a, b) {
if (a instanceof Array) {
if (b instanceof Array) {
return a.concat(b);
}
a.push(b);
return a;
}
if (b instanceof Array) {
b.push(a);
return b;
}
return [a, b];
};
}
clusterer = supercluster({
radius: json.data.clusteringRadius,
maxZoom: DEFAULT_MAX_ZOOM,
metricKey: 'metric',
metricReducer: reducer,
});
clusterer.load(json.data.geoJSON.features);
div.selectAll('*').remove();
ReactDOM.render(
<MapboxViz
{...json.data}
rgb={rgb}
sliceHeight={slice.height()}
sliceWidth={slice.width()}
clusterer={clusterer}
pointRadius={DEFAULT_POINT_RADIUS}
aggregatorName={aggName}
/>,
div.node()
);
slice.done(json);
});
};
return {
render,
resize() {},
};
div.selectAll('*').remove();
ReactDOM.render(
<MapboxViz
{...json.data}
rgb={rgb}
sliceHeight={slice.height()}
sliceWidth={slice.width()}
clusterer={clusterer}
pointRadius={DEFAULT_POINT_RADIUS}
aggregatorName={aggName}
/>,
div.node()
);
}
module.exports = mapbox;

View File

@ -2,21 +2,9 @@ const $ = require('jquery');
require('./markup.css');
function markupWidget(slice) {
function refresh() {
$('#code').attr('rows', '15');
$.getJSON(slice.jsonEndpoint(), function (payload) {
slice.container.html(payload.data.html);
slice.done(payload);
})
.fail(function (xhr) {
slice.error(xhr.responseText, xhr);
});
}
return {
render: refresh,
resize: refresh,
};
function markupWidget(slice, payload) {
$('#code').attr('rows', '15');
slice.container.html(payload.data.html);
}
module.exports = markupWidget;

View File

@ -53,346 +53,320 @@ const addTotalBarValues = function (chart, data, stacked) {
});
};
function nvd3Vis(slice) {
function nvd3Vis(slice, payload) {
let chart;
let colorKey = 'key';
const render = function () {
d3.json(slice.jsonEndpoint(), function (error, payload) {
slice.container.html('');
// Check error first, otherwise payload can be null
if (error) {
slice.error(error.responseText, error);
return;
slice.container.html('');
slice.clearError();
// Calculates the longest label size for stretching bottom margin
function calculateStretchMargins(payloadData) {
let stretchMargin = 0;
const pixelsPerCharX = 4.5; // approx, depends on font size
let maxLabelSize = 10; // accommodate for shorter labels
payloadData.data.forEach((d) => {
const axisLabels = d.values;
for (let i = 0; i < axisLabels.length; i++) {
maxLabelSize = Math.max(axisLabels[i].x.length, maxLabelSize);
}
slice.clearError();
// Calculates the longest label size for stretching bottom margin
function calculateStretchMargins(payloadData) {
let stretchMargin = 0;
const pixelsPerCharX = 4.5; // approx, depends on font size
let maxLabelSize = 10; // accommodate for shorter labels
payloadData.data.forEach((d) => {
const axisLabels = d.values;
for (let i = 0; i < axisLabels.length; i++) {
maxLabelSize = Math.max(axisLabels[i].x.length, maxLabelSize);
}
});
stretchMargin = Math.ceil(pixelsPerCharX * maxLabelSize);
return stretchMargin;
}
let width = slice.width();
const fd = payload.form_data;
const barchartWidth = function () {
let bars;
if (fd.bar_stacked) {
bars = d3.max(payload.data, function (d) { return d.values.length; });
} else {
bars = d3.sum(payload.data, function (d) { return d.values.length; });
}
if (bars * minBarWidth > width) {
return bars * minBarWidth;
}
return width;
};
const vizType = fd.viz_type;
const f = d3.format('.3s');
const reduceXTicks = fd.reduce_x_ticks || false;
let stacked = false;
let row;
const drawGraph = function () {
switch (vizType) {
case 'line':
if (fd.show_brush) {
chart = nv.models.lineWithFocusChart();
chart.focus.xScale(d3.time.scale.utc());
chart.x2Axis
.showMaxMin(fd.x_axis_showminmax)
.staggerLabels(false);
} else {
chart = nv.models.lineChart();
}
// To alter the tooltip header
// chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
chart.xScale(d3.time.scale.utc());
chart.interpolate(fd.line_interpolation);
chart.xAxis
.showMaxMin(fd.x_axis_showminmax)
.staggerLabels(false);
break;
case 'dual_line':
chart = nv.models.multiChart();
chart.interpolate('linear');
break;
case 'bar':
chart = nv.models.multiBarChart()
.showControls(fd.show_controls)
.groupSpacing(0.1);
if (!reduceXTicks) {
width = barchartWidth();
}
chart.width(width);
chart.xAxis
.showMaxMin(false)
.staggerLabels(true);
stacked = fd.bar_stacked;
chart.stacked(stacked);
if (fd.show_bar_value) {
setTimeout(function () {
addTotalBarValues(chart, payload.data, stacked);
}, animationTime);
}
break;
case 'dist_bar':
chart = nv.models.multiBarChart()
.showControls(fd.show_controls)
.reduceXTicks(reduceXTicks)
.rotateLabels(45)
.groupSpacing(0.1); // Distance between each group of bars.
chart.xAxis
.showMaxMin(false);
stacked = fd.bar_stacked;
chart.stacked(stacked);
if (fd.order_bars) {
payload.data.forEach((d) => {
d.values.sort(
function compare(a, b) {
if (a.x < b.x) return -1;
if (a.x > b.x) return 1;
return 0;
}
);
});
}
if (fd.show_bar_value) {
setTimeout(function () {
addTotalBarValues(chart, payload.data, stacked);
}, animationTime);
}
if (!reduceXTicks) {
width = barchartWidth();
}
chart.width(width);
break;
case 'pie':
chart = nv.models.pieChart();
colorKey = 'x';
chart.valueFormat(f);
if (fd.donut) {
chart.donut(true);
}
chart.labelsOutside(fd.labels_outside);
chart.labelThreshold(0.05) // Configure the minimum slice size for labels to show up
.labelType(fd.pie_label_type);
chart.cornerRadius(true);
break;
case 'column':
chart = nv.models.multiBarChart()
.reduceXTicks(false)
.rotateLabels(45);
break;
case 'compare':
chart = nv.models.cumulativeLineChart();
chart.xScale(d3.time.scale.utc());
chart.xAxis
.showMaxMin(false)
.staggerLabels(true);
break;
case 'bubble':
row = (col1, col2) => `<tr><td>${col1}</td><td>${col2}</td></tr>`;
chart = nv.models.scatterChart();
chart.showDistX(true);
chart.showDistY(true);
chart.tooltip.contentGenerator(function (obj) {
const p = obj.point;
let s = '<table>';
s += (
`<tr><td style="color: ${p.color};">` +
`<strong>${p[fd.entity]}</strong> (${p.group})` +
'</td></tr>');
s += row(fd.x, f(p.x));
s += row(fd.y, f(p.y));
s += row(fd.size, f(p.size));
s += '</table>';
return s;
});
chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
break;
case 'area':
chart = nv.models.stackedAreaChart();
chart.showControls(fd.show_controls);
chart.style(fd.stacked_style);
chart.xScale(d3.time.scale.utc());
chart.xAxis
.showMaxMin(false)
.staggerLabels(true);
break;
case 'box_plot':
colorKey = 'label';
chart = nv.models.boxPlotChart();
chart.x(function (d) {
return d.label;
});
chart.staggerLabels(true);
chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
break;
case 'bullet':
chart = nv.models.bulletChart();
break;
default:
throw new Error('Unrecognized visualization for nvd3' + vizType);
}
if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') {
chart.showLegend(fd.show_legend);
}
let height = slice.height() - 15;
if (vizType === 'bullet') {
height = Math.min(height, 50);
}
chart.height(height);
slice.container.css('height', height + 'px');
if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
chart.useInteractiveGuideline(true);
}
if (fd.y_axis_zero) {
chart.forceY([0]);
} else if (fd.y_log_scale) {
chart.yScale(d3.scale.log());
}
if (fd.x_log_scale) {
chart.xScale(d3.scale.log());
}
let xAxisFormatter;
if (vizType === 'bubble') {
xAxisFormatter = d3.format('.3s');
} else if (fd.x_axis_format === 'smart_date') {
xAxisFormatter = formatDate;
chart.xAxis.tickFormat(xAxisFormatter);
} else if (fd.x_axis_format !== undefined) {
xAxisFormatter = timeFormatFactory(fd.x_axis_format);
chart.xAxis.tickFormat(xAxisFormatter);
}
if (chart.hasOwnProperty('x2Axis')) {
chart.x2Axis.tickFormat(xAxisFormatter);
height += 30;
}
if (vizType === 'bubble') {
chart.xAxis.tickFormat(d3.format('.3s'));
} else if (fd.x_axis_format === 'smart_date') {
chart.xAxis.tickFormat(formatDate);
} else if (fd.x_axis_format !== undefined) {
chart.xAxis.tickFormat(timeFormatFactory(fd.x_axis_format));
}
if (chart.yAxis !== undefined) {
chart.yAxis.tickFormat(d3.format('.3s'));
}
if (fd.y_axis_format && chart.yAxis) {
chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
if (chart.y2Axis !== undefined) {
chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
}
}
if (vizType !== 'bullet') {
chart.color((d) => category21(d[colorKey]));
}
if (fd.x_axis_label && fd.x_axis_label !== '' && chart.xAxis) {
let distance = 0;
if (fd.bottom_margin) {
distance = fd.bottom_margin - 50;
}
chart.xAxis.axisLabel(fd.x_axis_label).axisLabelDistance(distance);
}
if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) {
chart.yAxis.axisLabel(fd.y_axis_label);
chart.margin({ left: 90 });
}
if (fd.bottom_margin === 'auto') {
if (vizType === 'dist_bar') {
const stretchMargin = calculateStretchMargins(payload);
chart.margin({ bottom: stretchMargin });
} else {
chart.margin({ bottom: 50 });
}
} else {
chart.margin({ bottom: fd.bottom_margin });
}
let svg = d3.select(slice.selector).select('svg');
if (svg.empty()) {
svg = d3.select(slice.selector).append('svg');
}
if (vizType === 'dual_line') {
chart.yAxis1.tickFormat(d3.format(fd.y_axis_format));
chart.yAxis2.tickFormat(d3.format(fd.y_axis_2_format));
chart.showLegend(true);
chart.margin({ right: 50 });
}
svg
.datum(payload.data)
.transition().duration(500)
.attr('height', height)
.attr('width', width)
.call(chart);
if (fd.show_markers) {
svg.selectAll('.nv-point')
.style('stroke-opacity', 1)
.style('fill-opacity', 1);
}
return chart;
};
const graph = drawGraph();
nv.addGraph(graph);
slice.done(payload);
});
};
stretchMargin = Math.ceil(pixelsPerCharX * maxLabelSize);
return stretchMargin;
}
const update = function () {
if (chart && chart.update) {
chart.height(slice.height());
chart.width(slice.width());
chart.update();
let width = slice.width();
const fd = payload.form_data;
const barchartWidth = function () {
let bars;
if (fd.bar_stacked) {
bars = d3.max(payload.data, function (d) { return d.values.length; });
} else {
bars = d3.sum(payload.data, function (d) { return d.values.length; });
}
if (bars * minBarWidth > width) {
return bars * minBarWidth;
}
return width;
};
const vizType = fd.viz_type;
const f = d3.format('.3s');
const reduceXTicks = fd.reduce_x_ticks || false;
let stacked = false;
let row;
return {
render,
resize: update,
const drawGraph = function () {
switch (vizType) {
case 'line':
if (fd.show_brush) {
chart = nv.models.lineWithFocusChart();
chart.focus.xScale(d3.time.scale.utc());
chart.x2Axis
.showMaxMin(fd.x_axis_showminmax)
.staggerLabels(false);
} else {
chart = nv.models.lineChart();
}
// To alter the tooltip header
// chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
chart.xScale(d3.time.scale.utc());
chart.interpolate(fd.line_interpolation);
chart.xAxis
.showMaxMin(fd.x_axis_showminmax)
.staggerLabels(false);
break;
case 'dual_line':
chart = nv.models.multiChart();
chart.interpolate('linear');
break;
case 'bar':
chart = nv.models.multiBarChart()
.showControls(fd.show_controls)
.groupSpacing(0.1);
if (!reduceXTicks) {
width = barchartWidth();
}
chart.width(width);
chart.xAxis
.showMaxMin(false)
.staggerLabels(true);
stacked = fd.bar_stacked;
chart.stacked(stacked);
if (fd.show_bar_value) {
setTimeout(function () {
addTotalBarValues(chart, payload.data, stacked);
}, animationTime);
}
break;
case 'dist_bar':
chart = nv.models.multiBarChart()
.showControls(fd.show_controls)
.reduceXTicks(reduceXTicks)
.rotateLabels(45)
.groupSpacing(0.1); // Distance between each group of bars.
chart.xAxis
.showMaxMin(false);
stacked = fd.bar_stacked;
chart.stacked(stacked);
if (fd.order_bars) {
payload.data.forEach((d) => {
d.values.sort(
function compare(a, b) {
if (a.x < b.x) return -1;
if (a.x > b.x) return 1;
return 0;
}
);
});
}
if (fd.show_bar_value) {
setTimeout(function () {
addTotalBarValues(chart, payload.data, stacked);
}, animationTime);
}
if (!reduceXTicks) {
width = barchartWidth();
}
chart.width(width);
break;
case 'pie':
chart = nv.models.pieChart();
colorKey = 'x';
chart.valueFormat(f);
if (fd.donut) {
chart.donut(true);
}
chart.labelsOutside(fd.labels_outside);
chart.labelThreshold(0.05) // Configure the minimum slice size for labels to show up
.labelType(fd.pie_label_type);
chart.cornerRadius(true);
break;
case 'column':
chart = nv.models.multiBarChart()
.reduceXTicks(false)
.rotateLabels(45);
break;
case 'compare':
chart = nv.models.cumulativeLineChart();
chart.xScale(d3.time.scale.utc());
chart.xAxis
.showMaxMin(false)
.staggerLabels(true);
break;
case 'bubble':
row = (col1, col2) => `<tr><td>${col1}</td><td>${col2}</td></tr>`;
chart = nv.models.scatterChart();
chart.showDistX(true);
chart.showDistY(true);
chart.tooltip.contentGenerator(function (obj) {
const p = obj.point;
let s = '<table>';
s += (
`<tr><td style="color: ${p.color};">` +
`<strong>${p[fd.entity]}</strong> (${p.group})` +
'</td></tr>');
s += row(fd.x, f(p.x));
s += row(fd.y, f(p.y));
s += row(fd.size, f(p.size));
s += '</table>';
return s;
});
chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
break;
case 'area':
chart = nv.models.stackedAreaChart();
chart.showControls(fd.show_controls);
chart.style(fd.stacked_style);
chart.xScale(d3.time.scale.utc());
chart.xAxis
.showMaxMin(false)
.staggerLabels(true);
break;
case 'box_plot':
colorKey = 'label';
chart = nv.models.boxPlotChart();
chart.x(function (d) {
return d.label;
});
chart.staggerLabels(true);
chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
break;
case 'bullet':
chart = nv.models.bulletChart();
break;
default:
throw new Error('Unrecognized visualization for nvd3' + vizType);
}
if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') {
chart.showLegend(fd.show_legend);
}
let height = slice.height() - 15;
if (vizType === 'bullet') {
height = Math.min(height, 50);
}
chart.height(height);
slice.container.css('height', height + 'px');
if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
chart.useInteractiveGuideline(true);
}
if (fd.y_axis_zero) {
chart.forceY([0]);
} else if (fd.y_log_scale) {
chart.yScale(d3.scale.log());
}
if (fd.x_log_scale) {
chart.xScale(d3.scale.log());
}
let xAxisFormatter;
if (vizType === 'bubble') {
xAxisFormatter = d3.format('.3s');
} else if (fd.x_axis_format === 'smart_date') {
xAxisFormatter = formatDate;
chart.xAxis.tickFormat(xAxisFormatter);
} else if (fd.x_axis_format !== undefined) {
xAxisFormatter = timeFormatFactory(fd.x_axis_format);
chart.xAxis.tickFormat(xAxisFormatter);
}
if (chart.hasOwnProperty('x2Axis')) {
chart.x2Axis.tickFormat(xAxisFormatter);
height += 30;
}
if (vizType === 'bubble') {
chart.xAxis.tickFormat(d3.format('.3s'));
} else if (fd.x_axis_format === 'smart_date') {
chart.xAxis.tickFormat(formatDate);
} else if (fd.x_axis_format !== undefined) {
chart.xAxis.tickFormat(timeFormatFactory(fd.x_axis_format));
}
if (chart.yAxis !== undefined) {
chart.yAxis.tickFormat(d3.format('.3s'));
}
if (fd.y_axis_format && chart.yAxis) {
chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
if (chart.y2Axis !== undefined) {
chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
}
}
if (vizType !== 'bullet') {
chart.color((d) => category21(d[colorKey]));
}
if (fd.x_axis_label && fd.x_axis_label !== '' && chart.xAxis) {
let distance = 0;
if (fd.bottom_margin) {
distance = fd.bottom_margin - 50;
}
chart.xAxis.axisLabel(fd.x_axis_label).axisLabelDistance(distance);
}
if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) {
chart.yAxis.axisLabel(fd.y_axis_label);
chart.margin({ left: 90 });
}
if (fd.bottom_margin === 'auto') {
if (vizType === 'dist_bar') {
const stretchMargin = calculateStretchMargins(payload);
chart.margin({ bottom: stretchMargin });
} else {
chart.margin({ bottom: 50 });
}
} else {
chart.margin({ bottom: fd.bottom_margin });
}
let svg = d3.select(slice.selector).select('svg');
if (svg.empty()) {
svg = d3.select(slice.selector).append('svg');
}
if (vizType === 'dual_line') {
chart.yAxis1.tickFormat(d3.format(fd.y_axis_format));
chart.yAxis2.tickFormat(d3.format(fd.y_axis_2_format));
chart.showLegend(true);
chart.margin({ right: 50 });
}
svg
.datum(payload.data)
.transition().duration(500)
.attr('height', height)
.attr('width', width)
.call(chart);
if (fd.show_markers) {
svg.selectAll('.nv-point')
.style('stroke-opacity', 1)
.style('fill-opacity', 1);
}
return chart;
};
const graph = drawGraph();
nv.addGraph(graph);
}
module.exports = nvd3Vis;

View File

@ -6,100 +6,87 @@ d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js');
require('../vendor/parallel_coordinates/d3.parcoords.css');
require('./parallel_coordinates.css');
function parallelCoordVis(slice) {
function refresh() {
$('#code').attr('rows', '15');
$.getJSON(slice.jsonEndpoint(), function (payload) {
const fd = payload.form_data;
const data = payload.data;
function parallelCoordVis(slice, payload) {
$('#code').attr('rows', '15');
const fd = payload.form_data;
const data = payload.data;
let cols = fd.metrics;
if (fd.include_series) {
cols = [fd.series].concat(fd.metrics);
}
const ttypes = {};
ttypes[fd.series] = 'string';
fd.metrics.forEach(function (v) {
ttypes[v] = 'number';
});
let ext = d3.extent(data, function (d) {
return d[fd.secondary_metric];
});
ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
const cScale = d3.scale.linear()
.domain(ext)
.range(['red', 'grey', 'blue'])
.interpolate(d3.interpolateLab);
const color = function (d) {
return cScale(d[fd.secondary_metric]);
};
const container = d3.select(slice.selector);
container.selectAll('*').remove();
const effHeight = fd.show_datatable ? (slice.height() / 2) : slice.height();
container.append('div')
.attr('id', 'parcoords_' + slice.container_id)
.style('height', effHeight + 'px')
.classed('parcoords', true);
const parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
.width(slice.width())
.color(color)
.alpha(0.5)
.composite('darken')
.height(effHeight)
.data(data)
.dimensions(cols)
.types(ttypes)
.render()
.createAxes()
.shadows()
.reorderable()
.brushMode('1D-axes');
if (fd.show_datatable) {
// create data table, row hover highlighting
const grid = d3.divgrid();
container.append('div')
.style('height', effHeight + 'px')
.datum(data)
.call(grid)
.classed('parcoords grid', true)
.selectAll('.row')
.on({
mouseover(d) {
parcoords.highlight([d]);
},
mouseout: parcoords.unhighlight,
});
// update data table on brush event
parcoords.on('brush', function (d) {
d3.select('.grid')
.datum(d)
.call(grid)
.selectAll('.row')
.on({
mouseover(dd) {
parcoords.highlight([dd]);
},
mouseout: parcoords.unhighlight,
});
});
}
slice.done(payload);
})
.fail(function (xhr) {
slice.error(xhr.responseText, xhr);
});
let cols = fd.metrics;
if (fd.include_series) {
cols = [fd.series].concat(fd.metrics);
}
return {
render: refresh,
resize: refresh,
const ttypes = {};
ttypes[fd.series] = 'string';
fd.metrics.forEach(function (v) {
ttypes[v] = 'number';
});
let ext = d3.extent(data, function (d) {
return d[fd.secondary_metric];
});
ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
const cScale = d3.scale.linear()
.domain(ext)
.range(['red', 'grey', 'blue'])
.interpolate(d3.interpolateLab);
const color = function (d) {
return cScale(d[fd.secondary_metric]);
};
const container = d3.select(slice.selector);
container.selectAll('*').remove();
const effHeight = fd.show_datatable ? (slice.height() / 2) : slice.height();
container.append('div')
.attr('id', 'parcoords_' + slice.container_id)
.style('height', effHeight + 'px')
.classed('parcoords', true);
const parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
.width(slice.width())
.color(color)
.alpha(0.5)
.composite('darken')
.height(effHeight)
.data(data)
.dimensions(cols)
.types(ttypes)
.render()
.createAxes()
.shadows()
.reorderable()
.brushMode('1D-axes');
if (fd.show_datatable) {
// create data table, row hover highlighting
const grid = d3.divgrid();
container.append('div')
.style('height', effHeight + 'px')
.datum(data)
.call(grid)
.classed('parcoords grid', true)
.selectAll('.row')
.on({
mouseover(d) {
parcoords.highlight([d]);
},
mouseout: parcoords.unhighlight,
});
// update data table on brush event
parcoords.on('brush', function (d) {
d3.select('.grid')
.datum(d)
.call(grid)
.selectAll('.row')
.on({
mouseover(dd) {
parcoords.highlight([dd]);
},
mouseout: parcoords.unhighlight,
});
});
}
}
module.exports = parallelCoordVis;

View File

@ -8,34 +8,22 @@ import 'datatables.net';
import dt from 'datatables.net-bs';
dt(window, $);
module.exports = function (slice) {
module.exports = function (slice, payload) {
const container = slice.container;
function refresh() {
$.getJSON(slice.jsonEndpoint(), function (json) {
const fd = json.form_data;
container.html(json.data);
if (fd.groupby.length === 1) {
const height = container.height();
const table = container.find('table').DataTable({
paging: false,
searching: false,
bInfo: false,
scrollY: height + 'px',
scrollCollapse: true,
scrollX: true,
});
table.column('-1').order('desc').draw();
fixDataTableBodyHeight(
container.find('.dataTables_wrapper'), height);
}
slice.done(json);
}).fail(function (xhr) {
slice.error(xhr.responseText, xhr);
const fd = payload.form_data;
container.html(payload.data);
if (fd.groupby.length === 1) {
const height = container.height();
const table = container.find('table').DataTable({
paging: false,
searching: false,
bInfo: false,
scrollY: height + 'px',
scrollCollapse: true,
scrollX: true,
});
table.column('-1').order('desc').draw();
fixDataTableBodyHeight(
container.find('.dataTables_wrapper'), height);
}
return {
render: refresh,
resize: refresh,
};
};

View File

@ -6,179 +6,164 @@ d3.sankey = require('d3-sankey').sankey;
require('./sankey.css');
function sankeyVis(slice) {
const render = function () {
const div = d3.select(slice.selector);
const margin = {
top: 5,
right: 5,
bottom: 5,
left: 5,
};
const width = slice.width() - margin.left - margin.right;
const height = slice.height() - margin.top - margin.bottom;
function sankeyVis(slice, payload) {
const div = d3.select(slice.selector);
const margin = {
top: 5,
right: 5,
bottom: 5,
left: 5,
};
const width = slice.width() - margin.left - margin.right;
const height = slice.height() - margin.top - margin.bottom;
const formatNumber = d3.format(',.2f');
const formatNumber = d3.format(',.2f');
div.selectAll('*').remove();
const svg = div.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
div.selectAll('*').remove();
const svg = div.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
const tooltip = div.append('div')
.attr('class', 'sankey-tooltip')
const tooltip = div.append('div')
.attr('class', 'sankey-tooltip')
.style('opacity', 0);
const sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
const path = sankey.link();
let nodes = {};
// Compute the distinct nodes from the links.
const links = payload.data.map(function (row) {
const link = Object.assign({}, row);
link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
link.value = Number(link.value);
return link;
});
nodes = d3.values(nodes);
sankey
.nodes(nodes)
.links(links)
.layout(32);
function getTooltipHtml(d) {
let html;
if (d.sourceLinks) { // is node
html = d.name + " Value: <span class='emph'>" + formatNumber(d.value) + '</span>';
} else {
const val = formatNumber(d.value);
const sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
const targetPercent = d3.round((d.value / d.target.value) * 100, 1);
html = [
"<div class=''>Path Value: <span class='emph'>", val, '</span></div>',
"<div class='percents'>",
"<span class='emph'>",
(isFinite(sourcePercent) ? sourcePercent : '100'),
'%</span> of ', d.source.name, '<br/>',
"<span class='emph'>" +
(isFinite(targetPercent) ? targetPercent : '--') +
'%</span> of ', d.target.name, 'target',
'</div>',
].join('');
}
return html;
}
function onmouseover(d) {
tooltip
.html(function () { return getTooltipHtml(d); })
.transition()
.duration(200)
.style('left', (d3.event.offsetX + 10) + 'px')
.style('top', (d3.event.offsetY + 10) + 'px')
.style('opacity', 0.95);
}
function onmouseout() {
tooltip.transition()
.duration(100)
.style('opacity', 0);
}
const sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
const link = svg.append('g').selectAll('.link')
.data(links)
.enter()
.append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke-width', (d) => Math.max(1, d.dy))
.sort((a, b) => b.dy - a.dy)
.on('mouseover', onmouseover)
.on('mouseout', onmouseout);
const path = sankey.link();
function dragmove(d) {
d3.select(this)
.attr(
'transform',
`translate(${d.x},${(d.y = Math.max(0, Math.min(height - d.dy, d3.event.y)))})`
);
sankey.relayout();
link.attr('d', path);
}
d3.json(slice.jsonEndpoint(), function (error, json) {
if (error !== null) {
slice.error(error.responseText, error);
return;
}
let nodes = {};
// Compute the distinct nodes from the links.
const links = json.data.map(function (row) {
const link = Object.assign({}, row);
link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
link.value = Number(link.value);
return link;
});
nodes = d3.values(nodes);
const node = svg.append('g').selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.call(d3.behavior.drag()
.origin(function (d) {
return d;
})
.on('dragstart', function () {
this.parentNode.appendChild(this);
})
.on('drag', dragmove)
);
sankey
.nodes(nodes)
.links(links)
.layout(32);
node.append('rect')
.attr('height', function (d) {
return d.dy;
})
.attr('width', sankey.nodeWidth())
.style('fill', function (d) {
d.color = category21(d.name.replace(/ .*/, ''));
return d.color;
})
.style('stroke', function (d) {
return d3.rgb(d.color).darker(2);
})
.on('mouseover', onmouseover)
.on('mouseout', onmouseout);
function getTooltipHtml(d) {
let html;
if (d.sourceLinks) { // is node
html = d.name + " Value: <span class='emph'>" + formatNumber(d.value) + '</span>';
} else {
const val = formatNumber(d.value);
const sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
const targetPercent = d3.round((d.value / d.target.value) * 100, 1);
html = [
"<div class=''>Path Value: <span class='emph'>", val, '</span></div>',
"<div class='percents'>",
"<span class='emph'>",
(isFinite(sourcePercent) ? sourcePercent : '100'),
'%</span> of ', d.source.name, '<br/>',
"<span class='emph'>" +
(isFinite(targetPercent) ? targetPercent : '--') +
'%</span> of ', d.target.name, 'target',
'</div>',
].join('');
}
return html;
}
function onmouseover(d) {
tooltip
.html(function () { return getTooltipHtml(d); })
.transition()
.duration(200)
.style('left', (d3.event.offsetX + 10) + 'px')
.style('top', (d3.event.offsetY + 10) + 'px')
.style('opacity', 0.95);
}
function onmouseout() {
tooltip.transition()
.duration(100)
.style('opacity', 0);
}
const link = svg.append('g').selectAll('.link')
.data(links)
.enter()
.append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke-width', (d) => Math.max(1, d.dy))
.sort((a, b) => b.dy - a.dy)
.on('mouseover', onmouseover)
.on('mouseout', onmouseout);
function dragmove(d) {
d3.select(this)
.attr(
'transform',
`translate(${d.x},${(d.y = Math.max(0, Math.min(height - d.dy, d3.event.y)))})`
);
sankey.relayout();
link.attr('d', path);
}
const node = svg.append('g').selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.call(d3.behavior.drag()
.origin(function (d) {
return d;
})
.on('dragstart', function () {
this.parentNode.appendChild(this);
})
.on('drag', dragmove)
);
node.append('rect')
.attr('height', function (d) {
return d.dy;
})
.attr('width', sankey.nodeWidth())
.style('fill', function (d) {
d.color = category21(d.name.replace(/ .*/, ''));
return d.color;
})
.style('stroke', function (d) {
return d3.rgb(d.color).darker(2);
})
.on('mouseover', onmouseover)
.on('mouseout', onmouseout);
node.append('text')
.attr('x', -6)
.attr('y', function (d) {
return d.dy / 2;
})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.text(function (d) {
return d.name;
})
.filter(function (d) {
return d.x < width / 2;
})
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start');
slice.done(json);
});
};
return {
render,
resize: render,
};
node.append('text')
.attr('x', -6)
.attr('y', function (d) {
return d.dy / 2;
})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.text(function (d) {
return d.name;
})
.filter(function (d) {
return d.x < width / 2;
})
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start');
}
module.exports = sankeyVis;

View File

@ -6,383 +6,368 @@ import { wrapSvgText } from '../javascripts/modules/utils';
require('./sunburst.css');
// Modified from http://bl.ocks.org/kerryrodden/7090426
function sunburstVis(slice) {
const render = function () {
const container = d3.select(slice.selector);
// vars with shared scope within this function
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
const containerWidth = slice.width();
const containerHeight = slice.height();
const breadcrumbHeight = containerHeight * 0.085;
const visWidth = containerWidth - margin.left - margin.right;
const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
const radius = Math.min(visWidth, visHeight) / 2;
function sunburstVis(slice, payload) {
const container = d3.select(slice.selector);
let colorByCategory = true; // color by category if primary/secondary metrics match
let maxBreadcrumbs;
let breadcrumbDims; // set based on data
let totalSize; // total size of all segments; set after loading the data.
let colorScale;
let breadcrumbs;
let vis;
let arcs;
let gMiddleText; // dom handles
// vars with shared scope within this function
const margin = { top: 10, right: 5, bottom: 10, left: 5 };
const containerWidth = slice.width();
const containerHeight = slice.height();
const breadcrumbHeight = containerHeight * 0.085;
const visWidth = containerWidth - margin.left - margin.right;
const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
const radius = Math.min(visWidth, visHeight) / 2;
// Helper + path gen functions
const partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function (d) { return d.m1; });
let colorByCategory = true; // color by category if primary/secondary metrics match
let maxBreadcrumbs;
let breadcrumbDims; // set based on data
let totalSize; // total size of all segments; set after loading the data.
let colorScale;
let breadcrumbs;
let vis;
let arcs;
let gMiddleText; // dom handles
const arc = d3.svg.arc()
.startAngle((d) => d.x)
.endAngle((d) => d.x + d.dx)
.innerRadius(function (d) {
return Math.sqrt(d.y);
})
.outerRadius(function (d) {
return Math.sqrt(d.y + d.dy);
// Helper + path gen functions
const partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function (d) { return d.m1; });
const arc = d3.svg.arc()
.startAngle((d) => d.x)
.endAngle((d) => d.x + d.dx)
.innerRadius(function (d) {
return Math.sqrt(d.y);
})
.outerRadius(function (d) {
return Math.sqrt(d.y + d.dy);
});
const formatNum = d3.format('.3s');
const formatPerc = d3.format('.3p');
container.select('svg').remove();
const svg = container.append('svg:svg')
.attr('width', containerWidth)
.attr('height', containerHeight);
function createBreadcrumbs(rawData) {
const firstRowData = rawData.data[0];
// -2 bc row contains 2x metrics, +extra for %label and buffer
maxBreadcrumbs = (firstRowData.length - 2) + 1;
breadcrumbDims = {
width: visWidth / maxBreadcrumbs,
height: breadcrumbHeight * 0.8, // more margin
spacing: 3,
tipTailWidth: 10,
};
breadcrumbs = svg.append('svg:g')
.attr('class', 'breadcrumbs')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
breadcrumbs.append('svg:text')
.attr('class', 'end-label');
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
const path = [];
let current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
const points = [];
points.push('0,0');
points.push(breadcrumbDims.width + ',0');
points.push(
breadcrumbDims.width + breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
points.push(breadcrumbDims.width + ',' + breadcrumbDims.height);
points.push('0,' + breadcrumbDims.height);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
}
return points.join(' ');
}
function updateBreadcrumbs(sequenceArray, percentageString) {
const g = breadcrumbs.selectAll('g')
.data(sequenceArray, function (d) {
return d.name + d.depth;
});
const formatNum = d3.format('.3s');
const formatPerc = d3.format('.3p');
// Add breadcrumb and label for entering nodes.
const entering = g.enter().append('svg:g');
container.select('svg').remove();
const svg = container.append('svg:svg')
.attr('width', containerWidth)
.attr('height', containerHeight);
function createBreadcrumbs(rawData) {
const firstRowData = rawData.data[0];
// -2 bc row contains 2x metrics, +extra for %label and buffer
maxBreadcrumbs = (firstRowData.length - 2) + 1;
breadcrumbDims = {
width: visWidth / maxBreadcrumbs,
height: breadcrumbHeight * 0.8, // more margin
spacing: 3,
tipTailWidth: 10,
};
breadcrumbs = svg.append('svg:g')
.attr('class', 'breadcrumbs')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
breadcrumbs.append('svg:text')
.attr('class', 'end-label');
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
const path = [];
let current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
const points = [];
points.push('0,0');
points.push(breadcrumbDims.width + ',0');
points.push(
breadcrumbDims.width + breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
points.push(breadcrumbDims.width + ',' + breadcrumbDims.height);
points.push('0,' + breadcrumbDims.height);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
}
return points.join(' ');
}
function updateBreadcrumbs(sequenceArray, percentageString) {
const g = breadcrumbs.selectAll('g')
.data(sequenceArray, function (d) {
return d.name + d.depth;
entering.append('svg:polygon')
.attr('points', breadcrumbPoints)
.style('fill', function (d) {
return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
});
// Add breadcrumb and label for entering nodes.
const entering = g.enter().append('svg:g');
entering.append('svg:polygon')
.attr('points', breadcrumbPoints)
.style('fill', function (d) {
return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
});
entering.append('svg:text')
.attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
.attr('y', breadcrumbDims.height / 4)
.attr('dy', '0.35em')
.style('fill', function (d) {
// Make text white or black based on the lightness of the background
const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
return col.l < 0.5 ? 'white' : 'black';
})
.attr('class', 'step-label')
.text(function (d) { return d.name.replace(/_/g, ' '); })
.call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
// Set position for entering and updating nodes.
g.attr('transform', function (d, i) {
return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)';
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
breadcrumbs.select('.end-label')
.attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
.attr('y', breadcrumbDims.height / 2)
.attr('dy', '0.35em')
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
breadcrumbs.style('visibility', null);
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseenter(d) {
const sequenceArray = getAncestors(d);
const parentOfD = sequenceArray[sequenceArray.length - 2] || null;
const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
const absolutePercString = formatPerc(absolutePercentage);
const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
// 3 levels of text if inner-most level, 4 otherwise
const yOffsets = ['-25', '7', '35', '60'];
let offsetIndex = 0;
// If metrics match, assume we are coloring by category
const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
gMiddleText.selectAll('*').remove();
gMiddleText.append('text')
.attr('class', 'path-abs-percent')
.attr('y', yOffsets[offsetIndex++])
.text(absolutePercString + ' of total');
if (conditionalPercString) {
gMiddleText.append('text')
.attr('class', 'path-cond-percent')
.attr('y', yOffsets[offsetIndex++])
.text(conditionalPercString + ' of parent');
}
gMiddleText.append('text')
.attr('class', 'path-metrics')
.attr('y', yOffsets[offsetIndex++])
.text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));
gMiddleText.append('text')
.attr('class', 'path-ratio')
.attr('y', yOffsets[offsetIndex++])
.text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));
// Reset and fade all the segments.
arcs.selectAll('path')
.style('stroke-width', null)
.style('stroke', null)
.style('opacity', 0.7);
// Then highlight only those that are an ancestor of the current segment.
arcs.selectAll('path')
.filter(function (node) {
return (sequenceArray.indexOf(node) >= 0);
entering.append('svg:text')
.attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
.attr('y', breadcrumbDims.height / 4)
.attr('dy', '0.35em')
.style('fill', function (d) {
// Make text white or black based on the lightness of the background
const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
return col.l < 0.5 ? 'white' : 'black';
})
.style('opacity', 1)
.style('stroke-width', '2px')
.style('stroke', '#000');
.attr('class', 'step-label')
.text(function (d) { return d.name.replace(/_/g, ' '); })
.call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
updateBreadcrumbs(sequenceArray, absolutePercString);
// Set position for entering and updating nodes.
g.attr('transform', function (d, i) {
return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)';
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
breadcrumbs.select('.end-label')
.attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
.attr('y', breadcrumbDims.height / 2)
.attr('dy', '0.35em')
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
breadcrumbs.style('visibility', null);
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseenter(d) {
const sequenceArray = getAncestors(d);
const parentOfD = sequenceArray[sequenceArray.length - 2] || null;
const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
const absolutePercString = formatPerc(absolutePercentage);
const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
// 3 levels of text if inner-most level, 4 otherwise
const yOffsets = ['-25', '7', '35', '60'];
let offsetIndex = 0;
// If metrics match, assume we are coloring by category
const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
gMiddleText.selectAll('*').remove();
gMiddleText.append('text')
.attr('class', 'path-abs-percent')
.attr('y', yOffsets[offsetIndex++])
.text(absolutePercString + ' of total');
if (conditionalPercString) {
gMiddleText.append('text')
.attr('class', 'path-cond-percent')
.attr('y', yOffsets[offsetIndex++])
.text(conditionalPercString + ' of parent');
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave() {
// Hide the breadcrumb trail
breadcrumbs.style('visibility', 'hidden');
gMiddleText.append('text')
.attr('class', 'path-metrics')
.attr('y', yOffsets[offsetIndex++])
.text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));
gMiddleText.selectAll('*').remove();
gMiddleText.append('text')
.attr('class', 'path-ratio')
.attr('y', yOffsets[offsetIndex++])
.text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));
// Deactivate all segments during transition.
arcs.selectAll('path').on('mouseenter', null);
// Reset and fade all the segments.
arcs.selectAll('path')
.style('stroke-width', null)
.style('stroke', null)
.style('opacity', 0.7);
// Transition each segment to full opacity and then reactivate it.
arcs.selectAll('path')
.transition()
.duration(200)
.style('opacity', 1)
.style('stroke', null)
.style('stroke-width', null)
.each('end', function () {
d3.select(this).on('mouseenter', mouseenter);
});
}
// Then highlight only those that are an ancestor of the current segment.
arcs.selectAll('path')
.filter(function (node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style('opacity', 1)
.style('stroke-width', '2px')
.style('stroke', '#000');
updateBreadcrumbs(sequenceArray, absolutePercString);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave() {
// Hide the breadcrumb trail
breadcrumbs.style('visibility', 'hidden');
gMiddleText.selectAll('*').remove();
// Deactivate all segments during transition.
arcs.selectAll('path').on('mouseenter', null);
// Transition each segment to full opacity and then reactivate it.
arcs.selectAll('path')
.transition()
.duration(200)
.style('opacity', 1)
.style('stroke', null)
.style('stroke-width', null)
.each('end', function () {
d3.select(this).on('mouseenter', mouseenter);
});
}
function buildHierarchy(rows) {
const root = {
name: 'root',
children: [],
};
function buildHierarchy(rows) {
const root = {
name: 'root',
children: [],
};
// each record [groupby1val, groupby2val, (<string> or 0)n, m1, m2]
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const m1 = Number(row[row.length - 2]);
const m2 = Number(row[row.length - 1]);
const levels = row.slice(0, row.length - 2);
if (isNaN(m1)) { // e.g. if this is a header row
continue;
}
let currentNode = root;
for (let level = 0; level < levels.length; level++) {
const children = currentNode.children || [];
const nodeName = levels[level];
// If the next node has the name '0', it will
const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0;
let childNode;
let currChild;
// each record [groupby1val, groupby2val, (<string> or 0)n, m1, m2]
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const m1 = Number(row[row.length - 2]);
const m2 = Number(row[row.length - 1]);
const levels = row.slice(0, row.length - 2);
if (isNaN(m1)) { // e.g. if this is a header row
continue;
}
let currentNode = root;
for (let level = 0; level < levels.length; level++) {
const children = currentNode.children || [];
const nodeName = levels[level];
// If the next node has the name '0', it will
const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0;
let childNode;
let currChild;
if (!isLeafNode) {
// Not yet at the end of the sequence; move down the tree.
let foundChild = false;
for (let k = 0; k < children.length; k++) {
currChild = children[k];
if (currChild.name === nodeName &&
currChild.level === level) {
// must match name AND level
if (!isLeafNode) {
// Not yet at the end of the sequence; move down the tree.
let foundChild = false;
for (let k = 0; k < children.length; k++) {
currChild = children[k];
if (currChild.name === nodeName &&
currChild.level === level) {
// must match name AND level
childNode = currChild;
foundChild = true;
break;
}
childNode = currChild;
foundChild = true;
break;
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {
name: nodeName,
children: [],
level,
};
children.push(childNode);
}
currentNode = childNode;
} else if (nodeName !== 0) {
// Reached the end of the sequence; create a leaf node.
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {
name: nodeName,
m1,
m2,
children: [],
level,
};
children.push(childNode);
}
currentNode = childNode;
} else if (nodeName !== 0) {
// Reached the end of the sequence; create a leaf node.
childNode = {
name: nodeName,
m1,
m2,
};
children.push(childNode);
}
}
}
function recurse(node) {
if (node.children) {
let sums;
let m1 = 0;
let m2 = 0;
for (let i = 0; i < node.children.length; i++) {
sums = recurse(node.children[i]);
m1 += sums[0];
m2 += sums[1];
}
node.m1 = m1;
node.m2 = m2;
function recurse(node) {
if (node.children) {
let sums;
let m1 = 0;
let m2 = 0;
for (let i = 0; i < node.children.length; i++) {
sums = recurse(node.children[i]);
m1 += sums[0];
m2 += sums[1];
}
return [node.m1, node.m2];
node.m1 = m1;
node.m2 = m2;
}
recurse(root);
return root;
return [node.m1, node.m2];
}
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(rawData) {
const tree = buildHierarchy(rawData.data);
recurse(root);
return root;
}
vis = svg.append('svg:g')
.attr('class', 'sunburst-vis')
.attr('transform', (
'translate(' +
`${(margin.left + (visWidth / 2))},` +
`${(margin.top + breadcrumbHeight + (visHeight / 2))}` +
')'
))
.on('mouseleave', mouseleave);
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(rawData) {
const tree = buildHierarchy(rawData.data);
arcs = vis.append('svg:g')
.attr('id', 'arcs');
vis = svg.append('svg:g')
.attr('class', 'sunburst-vis')
.attr('transform', (
'translate(' +
`${(margin.left + (visWidth / 2))},` +
`${(margin.top + breadcrumbHeight + (visHeight / 2))}` +
')'
))
.on('mouseleave', mouseleave);
gMiddleText = vis.append('svg:g')
.attr('class', 'center-label');
arcs = vis.append('svg:g')
.attr('id', 'arcs');
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
arcs.append('svg:circle')
.attr('r', radius)
.style('opacity', 0);
gMiddleText = vis.append('svg:g')
.attr('class', 'center-label');
// For efficiency, filter nodes to keep only those large enough to see.
const nodes = partition.nodes(tree)
.filter(function (d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
arcs.append('svg:circle')
.attr('r', radius)
.style('opacity', 0);
let ext;
// For efficiency, filter nodes to keep only those large enough to see.
const nodes = partition.nodes(tree)
.filter(function (d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
colorByCategory = false;
ext = d3.extent(nodes, (d) => d.m2 / d.m1);
colorScale = d3.scale.linear()
.domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
.range(['#00D1C1', 'white', '#FFB400']);
}
let ext;
const path = arcs.data([tree]).selectAll('path')
.data(nodes)
.enter()
.append('svg:path')
.attr('display', function (d) {
return d.depth ? null : 'none';
})
.attr('d', arc)
.attr('fill-rule', 'evenodd')
.style('fill', (d) => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
.style('opacity', 1)
.on('mouseenter', mouseenter);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
colorByCategory = false;
ext = d3.extent(nodes, (d) => d.m2 / d.m1);
colorScale = d3.scale.linear()
.domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
.range(['#00D1C1', 'white', '#FFB400']);
}
const path = arcs.data([tree]).selectAll('path')
.data(nodes)
.enter()
.append('svg:path')
.attr('display', function (d) {
return d.depth ? null : 'none';
})
.attr('d', arc)
.attr('fill-rule', 'evenodd')
.style('fill', (d) => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
.style('opacity', 1)
.on('mouseenter', mouseenter);
d3.json(slice.jsonEndpoint(), function (error, rawData) {
if (error !== null) {
slice.error(error.responseText, error);
return;
}
createBreadcrumbs(rawData);
createVisualization(rawData);
slice.done(rawData);
});
};
return {
render,
resize: render,
};
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
createBreadcrumbs(payload);
createVisualization(payload);
}
module.exports = sunburstVis;

View File

@ -10,156 +10,141 @@ import 'datatables.net';
import dt from 'datatables.net-bs';
dt(window, $);
function tableVis(slice) {
function tableVis(slice, payload) {
const container = $(slice.selector);
const fC = d3.format('0,000');
let timestampFormatter;
function refresh() {
const container = $(slice.selector);
function onError(xhr) {
slice.error(xhr.responseText, xhr);
return;
const data = payload.data;
const fd = payload.form_data;
// Removing metrics (aggregates) that are strings
const realMetrics = [];
for (const k in data.records[0]) {
if (fd.metrics.indexOf(k) > -1 && !isNaN(data.records[0][k])) {
realMetrics.push(k);
}
function onSuccess(json) {
const data = json.data;
const fd = json.form_data;
// Removing metrics (aggregates) that are strings
const realMetrics = [];
for (const k in data.records[0]) {
if (fd.metrics.indexOf(k) > -1 && !isNaN(data.records[0][k])) {
realMetrics.push(k);
}
}
const metrics = realMetrics;
}
const metrics = realMetrics;
function col(c) {
const arr = [];
for (let i = 0; i < data.records.length; i++) {
arr.push(data.records[i][c]);
}
return arr;
}
const maxes = {};
for (let i = 0; i < metrics.length; i++) {
maxes[metrics[i]] = d3.max(col(metrics[i]));
}
if (fd.table_timestamp_format === 'smart_date') {
timestampFormatter = formatDate;
} else if (fd.table_timestamp_format !== undefined) {
timestampFormatter = timeFormatFactory(fd.table_timestamp_format);
}
const div = d3.select(slice.selector);
div.html('');
const table = div.append('table')
.classed(
'dataframe dataframe table table-striped table-bordered ' +
'table-condensed table-hover dataTable no-footer', true)
.attr('width', '100%');
table.append('thead').append('tr')
.selectAll('th')
.data(data.columns)
.enter()
.append('th')
.text(function (d) {
return d;
});
table.append('tbody')
.selectAll('tr')
.data(data.records)
.enter()
.append('tr')
.selectAll('td')
.data((row) => data.columns.map((c) => {
let val = row[c];
if (c === 'timestamp') {
val = timestampFormatter(val);
}
return {
col: c,
val,
isMetric: metrics.indexOf(c) >= 0,
};
}))
.enter()
.append('td')
.style('background-image', function (d) {
if (d.isMetric) {
const perc = Math.round((d.val / maxes[d.col]) * 100);
return (
`linear-gradient(to right, lightgrey, lightgrey ${perc}%, ` +
`rgba(0,0,0,0) ${perc}%`
);
}
return null;
})
.attr('title', (d) => {
if (!isNaN(d.val)) {
return fC(d.val);
}
return null;
})
.attr('data-sort', function (d) {
return (d.isMetric) ? d.val : null;
})
.on('click', function (d) {
if (!d.isMetric && fd.table_filter) {
const td = d3.select(this);
if (td.classed('filtered')) {
slice.removeFilter(d.col, [d.val]);
d3.select(this).classed('filtered', false);
} else {
d3.select(this).classed('filtered', true);
slice.addFilter(d.col, [d.val]);
}
}
})
.style('cursor', function (d) {
return (!d.isMetric) ? 'pointer' : '';
})
.html((d) => {
if (d.isMetric) {
return slice.d3format(d.col, d.val);
}
return d.val;
});
const height = slice.height();
let paging = false;
let pageLength;
if (fd.page_length && fd.page_length > 0) {
paging = true;
pageLength = parseInt(fd.page_length, 10);
}
const datatable = container.find('.dataTable').DataTable({
paging,
pageLength,
aaSorting: [],
searching: fd.include_search,
bInfo: false,
scrollY: height + 'px',
scrollCollapse: true,
scrollX: true,
});
fixDataTableBodyHeight(
container.find('.dataTables_wrapper'), height);
// Sorting table by main column
if (fd.metrics.length > 0) {
const mainMetric = fd.metrics[0];
datatable.column(data.columns.indexOf(mainMetric)).order('desc').draw();
}
slice.done(json);
container.parents('.widget').find('.tooltip').remove();
function col(c) {
const arr = [];
for (let i = 0; i < data.records.length; i++) {
arr.push(data.records[i][c]);
}
$.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError);
return arr;
}
const maxes = {};
for (let i = 0; i < metrics.length; i++) {
maxes[metrics[i]] = d3.max(col(metrics[i]));
}
return {
render: refresh,
resize() {},
};
if (fd.table_timestamp_format === 'smart_date') {
timestampFormatter = formatDate;
} else if (fd.table_timestamp_format !== undefined) {
timestampFormatter = timeFormatFactory(fd.table_timestamp_format);
}
const div = d3.select(slice.selector);
div.html('');
const table = div.append('table')
.classed(
'dataframe dataframe table table-striped table-bordered ' +
'table-condensed table-hover dataTable no-footer', true)
.attr('width', '100%');
table.append('thead').append('tr')
.selectAll('th')
.data(data.columns)
.enter()
.append('th')
.text(function (d) {
return d;
});
table.append('tbody')
.selectAll('tr')
.data(data.records)
.enter()
.append('tr')
.selectAll('td')
.data((row) => data.columns.map((c) => {
let val = row[c];
if (c === 'timestamp') {
val = timestampFormatter(val);
}
return {
col: c,
val,
isMetric: metrics.indexOf(c) >= 0,
};
}))
.enter()
.append('td')
.style('background-image', function (d) {
if (d.isMetric) {
const perc = Math.round((d.val / maxes[d.col]) * 100);
return (
`linear-gradient(to right, lightgrey, lightgrey ${perc}%, ` +
`rgba(0,0,0,0) ${perc}%`
);
}
return null;
})
.attr('title', (d) => {
if (!isNaN(d.val)) {
return fC(d.val);
}
return null;
})
.attr('data-sort', function (d) {
return (d.isMetric) ? d.val : null;
})
.on('click', function (d) {
if (!d.isMetric && fd.table_filter) {
const td = d3.select(this);
if (td.classed('filtered')) {
slice.removeFilter(d.col, [d.val]);
d3.select(this).classed('filtered', false);
} else {
d3.select(this).classed('filtered', true);
slice.addFilter(d.col, [d.val]);
}
}
})
.style('cursor', function (d) {
return (!d.isMetric) ? 'pointer' : '';
})
.html((d) => {
if (d.isMetric) {
return slice.d3format(d.col, d.val);
}
return d.val;
});
const height = slice.height();
let paging = false;
let pageLength;
if (fd.page_length && fd.page_length > 0) {
paging = true;
pageLength = parseInt(fd.page_length, 10);
}
const datatable = container.find('.dataTable').DataTable({
paging,
pageLength,
aaSorting: [],
searching: fd.include_search,
bInfo: false,
scrollY: height + 'px',
scrollCollapse: true,
scrollX: true,
});
fixDataTableBodyHeight(
container.find('.dataTables_wrapper'), height);
// Sorting table by main column
if (fd.metrics.length > 0) {
const mainMetric = fd.metrics[0];
datatable.column(data.columns.indexOf(mainMetric)).order('desc').draw();
}
container.parents('.widget').find('.tooltip').remove();
}
module.exports = tableVis;

View File

@ -5,9 +5,8 @@ import { category21 } from '../javascripts/modules/colors';
require('./treemap.css');
/* Modified from http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022 */
function treemap(slice) {
let div;
function treemap(slice, payload) {
const div = d3.select(slice.selector);
const _draw = function (data, eltWidth, eltHeight, formData) {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
const navBarHeight = 36;
@ -226,31 +225,13 @@ function treemap(slice) {
display(data);
};
const render = function () {
div = d3.select(slice.selector);
d3.json(slice.jsonEndpoint(), function (error, json) {
if (error !== null) {
slice.error(error.responseText, error);
return;
}
div.selectAll('*').remove();
const width = slice.width();
// facet muliple metrics (no sense in combining)
const height = slice.height() / json.data.length;
for (let i = 0, l = json.data.length; i < l; i ++) {
_draw(json.data[i], width, height, json.form_data);
}
slice.done(json);
});
};
return {
render,
resize: render,
};
div.selectAll('*').remove();
const width = slice.width();
const height = slice.height() / payload.data.length;
for (let i = 0, l = payload.data.length; i < l; i ++) {
_draw(payload.data[i], width, height, payload.form_data);
}
}
module.exports = treemap;

View File

@ -3,74 +3,60 @@ import d3 from 'd3';
import cloudLayout from 'd3-cloud';
import { category21 } from '../javascripts/modules/colors';
function wordCloudChart(slice) {
function refresh() {
const chart = d3.select(slice.selector);
d3.json(slice.jsonEndpoint(), function (error, json) {
if (error !== null) {
slice.error(error.responseText, error);
return;
}
const data = json.data;
const range = [
json.form_data.size_from,
json.form_data.size_to,
];
const rotation = json.form_data.rotation;
let fRotation;
if (rotation === 'square') {
fRotation = () => ~~(Math.random() * 2) * 90;
} else if (rotation === 'flat') {
fRotation = () => 0;
} else {
fRotation = () => (~~(Math.random() * 6) - 3) * 30;
}
const size = [slice.width(), slice.height()];
function wordCloudChart(slice, payload) {
const chart = d3.select(slice.selector);
const data = payload.data;
const range = [
payload.form_data.size_from,
payload.form_data.size_to,
];
const rotation = payload.form_data.rotation;
let fRotation;
if (rotation === 'square') {
fRotation = () => ~~(Math.random() * 2) * 90;
} else if (rotation === 'flat') {
fRotation = () => 0;
} else {
fRotation = () => (~~(Math.random() * 6) - 3) * 30;
}
const size = [slice.width(), slice.height()];
const scale = d3.scale.linear()
.range(range)
.domain(d3.extent(data, function (d) {
return d.size;
}));
const scale = d3.scale.linear()
.range(range)
.domain(d3.extent(data, function (d) {
return d.size;
}));
function draw(words) {
chart.selectAll('*').remove();
function draw(words) {
chart.selectAll('*').remove();
chart.append('svg')
.attr('width', layout.size()[0])
.attr('height', layout.size()[1])
.append('g')
.attr('transform', `translate(${layout.size()[0] / 2},${layout.size()[1] / 2})`)
.selectAll('text')
.data(words)
.enter()
.append('text')
.style('font-size', (d) => d.size + 'px')
.style('font-family', 'Impact')
.style('fill', (d) => category21(d.text))
.attr('text-anchor', 'middle')
.attr('transform', (d) => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
.text((d) => d.text);
}
const layout = cloudLayout()
.size(size)
.words(data)
.padding(5)
.rotate(fRotation)
.font('serif')
.fontSize((d) => scale(d.size))
.on('end', draw);
layout.start();
slice.done(json);
});
chart.append('svg')
.attr('width', layout.size()[0])
.attr('height', layout.size()[1])
.append('g')
.attr('transform', `translate(${layout.size()[0] / 2},${layout.size()[1] / 2})`)
.selectAll('text')
.data(words)
.enter()
.append('text')
.style('font-size', (d) => d.size + 'px')
.style('font-family', 'Impact')
.style('fill', (d) => category21(d.text))
.attr('text-anchor', 'middle')
.attr('transform', (d) => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
.text((d) => d.text);
}
return {
render: refresh,
resize: refresh,
};
const layout = cloudLayout()
.size(size)
.words(data)
.padding(5)
.rotate(fRotation)
.font('serif')
.fontSize((d) => scale(d.size))
.on('end', draw);
layout.start();
}
module.exports = wordCloudChart;

View File

@ -5,102 +5,90 @@ const Datamap = require('datamaps');
// CSS
require('./world_map.css');
function worldMapChart(slice) {
const render = function () {
const container = slice.container;
const div = d3.select(slice.selector);
function worldMapChart(slice, payload) {
const container = slice.container;
const div = d3.select(slice.selector);
container.css('height', slice.height());
container.css('height', slice.height());
div.selectAll('*').remove();
const fd = payload.form_data;
// Ignore XXX's to get better normalization
let data = payload.data.filter((d) => (d.country && d.country !== 'XXX'));
d3.json(slice.jsonEndpoint(), function (error, json) {
div.selectAll('*').remove();
if (error !== null) {
slice.error(error.responseText, error);
return;
}
const fd = json.form_data;
// Ignore XXX's to get better normalization
let data = json.data.filter((d) => (d.country && d.country !== 'XXX'));
const ext = d3.extent(data, function (d) {
return d.m1;
});
const extRadius = d3.extent(data, function (d) {
return d.m2;
});
const radiusScale = d3.scale.linear()
.domain([extRadius[0], extRadius[1]])
.range([1, fd.max_bubble_size]);
const ext = d3.extent(data, function (d) {
return d.m1;
});
const extRadius = d3.extent(data, function (d) {
return d.m2;
});
const radiusScale = d3.scale.linear()
.domain([extRadius[0], extRadius[1]])
.range([1, fd.max_bubble_size]);
const colorScale = d3.scale.linear()
.domain([ext[0], ext[1]])
.range(['#FFF', 'black']);
const colorScale = d3.scale.linear()
.domain([ext[0], ext[1]])
.range(['#FFF', 'black']);
data = data.map((d) => Object.assign({}, d, {
radius: radiusScale(d.m2),
fillColor: colorScale(d.m1),
}));
data = data.map((d) => Object.assign({}, d, {
radius: radiusScale(d.m2),
fillColor: colorScale(d.m1),
}));
const mapData = {};
data.forEach((d) => {
mapData[d.country] = d;
});
const mapData = {};
data.forEach((d) => {
mapData[d.country] = d;
});
const f = d3.format('.3s');
const f = d3.format('.3s');
container.show();
container.show();
const map = new Datamap({
element: slice.container.get(0),
data,
fills: {
defaultFill: '#ddd',
},
geographyConfig: {
popupOnHover: true,
highlightOnHover: true,
borderWidth: 1,
borderColor: '#fff',
highlightBorderColor: '#fff',
highlightFillColor: '#005a63',
highlightBorderWidth: 1,
popupTemplate: (geo, d) => (
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m1)}</div>`
),
},
bubblesConfig: {
borderWidth: 1,
borderOpacity: 1,
borderColor: '#005a63',
popupOnHover: true,
radius: null,
popupTemplate: (geo, d) => (
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m2)}</div>`
),
fillOpacity: 0.5,
animate: true,
highlightOnHover: true,
highlightFillColor: '#005a63',
highlightBorderColor: 'black',
highlightBorderWidth: 2,
highlightBorderOpacity: 1,
highlightFillOpacity: 0.85,
exitDelay: 100,
key: JSON.stringify,
},
});
const map = new Datamap({
element: slice.container.get(0),
data,
fills: {
defaultFill: '#ddd',
},
geographyConfig: {
popupOnHover: true,
highlightOnHover: true,
borderWidth: 1,
borderColor: '#fff',
highlightBorderColor: '#fff',
highlightFillColor: '#005a63',
highlightBorderWidth: 1,
popupTemplate: (geo, d) => (
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m1)}</div>`
),
},
bubblesConfig: {
borderWidth: 1,
borderOpacity: 1,
borderColor: '#005a63',
popupOnHover: true,
radius: null,
popupTemplate: (geo, d) => (
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m2)}</div>`
),
fillOpacity: 0.5,
animate: true,
highlightOnHover: true,
highlightFillColor: '#005a63',
highlightBorderColor: 'black',
highlightBorderWidth: 2,
highlightBorderOpacity: 1,
highlightFillOpacity: 0.85,
exitDelay: 100,
key: JSON.stringify,
},
});
map.updateChoropleth(mapData);
map.updateChoropleth(mapData);
if (fd.show_bubbles) {
map.bubbles(data);
div.selectAll('circle.datamaps-bubble').style('fill', '#005a63');
}
slice.done(json);
});
};
return { render, resize: render };
if (fd.show_bubbles) {
map.bubbles(data);
div.selectAll('circle.datamaps-bubble').style('fill', '#005a63');
}
}
module.exports = worldMapChart;

View File

@ -357,8 +357,14 @@ class BaseViz(object):
if not payload:
is_cached = False
cache_timeout = self.cache_timeout
data = self.get_data()
try:
data = self.get_data()
except Exception as e:
logging.exception(e)
if not self.error_message:
self.error_message = str(e)
self.status = utils.QueryStatus.FAILED
data = None
payload = {
'cache_key': cache_key,
'cache_timeout': cache_timeout,