Simplifying the viz interface (#2005)
This commit is contained in:
parent
1c338ba742
commit
e46ba2b4a4
|
|
@ -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));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue