mirror of https://github.com/apache/superset.git
Histogram (#888)
* Add Histogram as a visualization The css and js file use the histogram code from https://bl.ocks.org/mbostock/3048450. THe viz.py extends from BaseViz to create chart data only for one histogram * using d3.layout.histogram * CSS updated The new css has been used from the d3 chart http://bl.ocks.org/mbostock/1933560 * bars are visible * added semicolons * histogram from http://bl.ocks.org/mbostock/1933560 It takes as input no of bins. The histogram cycles through a set of colors for different lengths of the bar. It places a y axis coordinate on top or on the upper end of the bar whichever is suitable. * update style changes
This commit is contained in:
parent
d15c557cd6
commit
15ee6d82e3
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
|
@ -32,6 +32,7 @@ const sourceMap = {
|
|||
cal_heatmap: 'cal_heatmap.js',
|
||||
horizon: 'horizon.js',
|
||||
mapbox: 'mapbox.jsx',
|
||||
histogram: 'histogram.js',
|
||||
};
|
||||
const color = function () {
|
||||
// Color related utility functions go in this object
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.axis line {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.axis text {
|
||||
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.axis path, .axis line {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// JS
|
||||
const d3 = require('d3')
|
||||
const px = window.px || require('../javascripts/modules/caravel.js')
|
||||
|
||||
// CSS
|
||||
require('./histogram.css')
|
||||
|
||||
function histogram(slice) {
|
||||
|
||||
const div = d3.select(slice.selector)
|
||||
|
||||
const _draw = function(data, numBins) {
|
||||
|
||||
// Set Margins
|
||||
const margin = {
|
||||
top: 50,
|
||||
right: 10,
|
||||
bottom: 20,
|
||||
left: 50,
|
||||
};
|
||||
const navBarHeight = 36;
|
||||
const navBarTitleSize = 12;
|
||||
const navBarBuffer = 10;
|
||||
const width = slice.width() - margin.left - margin.right;
|
||||
const height = slice.height() - margin.top - margin.bottom - navBarHeight - navBarBuffer;
|
||||
|
||||
// Set Histogram objects
|
||||
const formatNumber = d3.format(',.0f');
|
||||
const formatTicks = d3.format(',.00f');
|
||||
const x = d3.scale.ordinal();
|
||||
const y = d3.scale.linear();
|
||||
const xAxis = d3.svg.axis().scale(x).orient('bottom').ticks(numBins).tickFormat(formatTicks);
|
||||
const yAxis = d3.svg.axis().scale(y).orient('left').ticks(numBins*3);
|
||||
// Calculate bins for the data
|
||||
const bins = d3.layout.histogram().bins(numBins)(data);
|
||||
|
||||
// Set the x-values
|
||||
x.domain(bins.map(function(d) { return d.x;}))
|
||||
.rangeRoundBands([0, width], .1);
|
||||
// Set the y-values
|
||||
y.domain([0, d3.max(bins, function(d) { return d.y;})])
|
||||
.range([height, 0]);
|
||||
|
||||
// Create the svg value with the bins
|
||||
const svg = div.selectAll('svg').data([bins]).enter().append('svg');
|
||||
|
||||
// Make a rectangular background fill
|
||||
svg.append('rect')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('fill', '#f6f6f6');
|
||||
|
||||
// Transform the svg to make space for the margins
|
||||
const gEnter = svg
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
// Add the bars and the x axis
|
||||
gEnter.append('g').attr('class', 'bars');
|
||||
gEnter.append('g').attr('class', 'x axis');
|
||||
|
||||
// Add width and height to the svg
|
||||
svg.attr('width', slice.width())
|
||||
.attr('height', slice.height());
|
||||
|
||||
// Create the bars in the svg
|
||||
const bar = svg.select('.bars').selectAll('.bar').data(bins);
|
||||
bar.enter().append('rect');
|
||||
bar.exit().remove();
|
||||
// Set the Height and Width for each bar
|
||||
bar .attr('width', x.rangeBand())
|
||||
.attr('x', function(d) { return x(d.x); })
|
||||
.attr('y', function(d) { return y(d.y); })
|
||||
.attr('height', function(d) {
|
||||
return y.range()[0] - y(d.y);
|
||||
})
|
||||
.attr('fill', function(d) { return px.color.category21(d.length); })
|
||||
.order();
|
||||
|
||||
// Find maximum length to position the ticks on top of the bar correctly
|
||||
const maxLength = d3.max(bins, function(d) { return d.length;});
|
||||
function textAboveBar(d) {
|
||||
return d.length/maxLength < 0.1;
|
||||
}
|
||||
|
||||
// Add a bar text to each bar in the histogram
|
||||
svg.selectAll('.bartext')
|
||||
.data(bins)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('dy', '.75em')
|
||||
.attr('y', function(d) {
|
||||
let padding = 0.0
|
||||
if (textAboveBar(d)) {
|
||||
padding = 12.0
|
||||
} else {
|
||||
padding = -8.0
|
||||
}
|
||||
return y(d.y) - padding;
|
||||
})
|
||||
.attr('x', function(d) { return x(d.x) + (x.rangeBand()/2);})
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '15px')
|
||||
.text(function(d) { return formatNumber(d.y); })
|
||||
.attr('fill', function(d) {
|
||||
if(textAboveBar(d)) { return 'black'; } else { return 'white'; }
|
||||
})
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
// Update the x-axis
|
||||
svg.append('g')
|
||||
.attr('class', 'axis')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + (height + margin.top) + ')')
|
||||
.text('values')
|
||||
.call(xAxis);
|
||||
|
||||
// Update the Y Axis and add minor lines
|
||||
svg.append('g')
|
||||
.attr('class', 'axis')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
|
||||
.text('count')
|
||||
.call(yAxis)
|
||||
.selectAll('g')
|
||||
.filter(function(d) { return d; })
|
||||
.classed('minor', true);
|
||||
};
|
||||
|
||||
const render = function() {
|
||||
|
||||
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: render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = histogram;
|
||||
|
|
@ -1201,6 +1201,74 @@ class DistributionPieViz(NVD3Viz):
|
|||
return df.to_dict(orient="records")
|
||||
|
||||
|
||||
class HistogramViz(BaseViz):
|
||||
|
||||
"""Histogram"""
|
||||
|
||||
viz_type = "histogram"
|
||||
verbose_name = _("Histogram")
|
||||
is_timeseries = False
|
||||
fieldsets = ({
|
||||
'label': None,
|
||||
'fields': (
|
||||
('all_columns_x',),
|
||||
'row_limit',
|
||||
)
|
||||
}, {
|
||||
'label': _("Histogram Options"),
|
||||
'fields': (
|
||||
'link_length',
|
||||
)
|
||||
},)
|
||||
|
||||
form_overrides = {
|
||||
'all_columns_x': {
|
||||
'label': _('Numeric Column'),
|
||||
'description': _("Select the numeric column to draw the histogram"),
|
||||
},
|
||||
'link_length': {
|
||||
'label': _("No of Bins"),
|
||||
'description': _("Select number of bins for the histogram"),
|
||||
'default': 5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def query_obj(self):
|
||||
"""Returns the query object for this visualization"""
|
||||
d = super(HistogramViz, self).query_obj()
|
||||
d['row_limit'] = self.form_data.get('row_limit', int(config.get('ROW_LIMIT')))
|
||||
numeric_column = self.form_data.get('all_columns_x')
|
||||
if numeric_column is None:
|
||||
raise Exception("Must have one numeric column specified")
|
||||
d['columns'] = [numeric_column]
|
||||
return d
|
||||
|
||||
|
||||
def get_df(self, query_obj=None):
|
||||
"""Returns a pandas dataframe based on the query object"""
|
||||
if not query_obj:
|
||||
query_obj = self.query_obj()
|
||||
|
||||
self.results = self.datasource.query(**query_obj)
|
||||
self.query = self.results.query
|
||||
df = self.results.df
|
||||
|
||||
if df is None or df.empty:
|
||||
raise Exception("No data, to build histogram")
|
||||
|
||||
df.replace([np.inf, -np.inf], np.nan)
|
||||
df = df.fillna(0)
|
||||
return df
|
||||
|
||||
|
||||
def get_data(self):
|
||||
"""Returns the chart data"""
|
||||
df = self.get_df()
|
||||
chart_data = df[df.columns[0]].values.tolist()
|
||||
return chart_data
|
||||
|
||||
|
||||
class DistributionBarViz(DistributionPieViz):
|
||||
|
||||
"""A good old bar chart"""
|
||||
|
@ -1921,6 +1989,7 @@ viz_types_list = [
|
|||
CalHeatmapViz,
|
||||
HorizonViz,
|
||||
MapboxViz,
|
||||
HistogramViz,
|
||||
SeparatorViz,
|
||||
]
|
||||
|
||||
|
|
|
@ -81,3 +81,6 @@ Gallery
|
|||
|
||||
.. image:: _static/img/viz_thumbnails/separator.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/histogram.png
|
||||
:scale: 25 %
|
||||
|
|
Loading…
Reference in New Issue