Skip to content

Commit

Permalink
Histogram (#888)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shkr authored and mistercrunch committed Aug 10, 2016
1 parent d15c557 commit 15ee6d8
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 0 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions caravel/assets/javascripts/modules/caravel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions caravel/assets/visualizations/histogram.css
Original file line number Diff line number Diff line change
@@ -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;
}
152 changes: 152 additions & 0 deletions caravel/assets/visualizations/histogram.js
Original file line number Diff line number Diff line change
@@ -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;

69 changes: 69 additions & 0 deletions caravel/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,74 @@ def get_data(self):
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"""
Expand Down Expand Up @@ -1921,6 +1989,7 @@ def get_data(self):
CalHeatmapViz,
HorizonViz,
MapboxViz,
HistogramViz,
SeparatorViz,
]

Expand Down
3 changes: 3 additions & 0 deletions docs/gallery.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ Gallery

.. image:: _static/img/viz_thumbnails/separator.png
:scale: 25 %

.. image:: _static/img/viz_thumbnails/histogram.png
:scale: 25 %

0 comments on commit 15ee6d8

Please sign in to comment.