Merge pull request #1514 from Br3nda/charts

Use ChartKick gem for charts
This commit is contained in:
Shiny
2018-01-11 14:58:04 +13:00
committed by GitHub
25 changed files with 149 additions and 594 deletions

View File

@@ -78,8 +78,7 @@ gem 'omniauth-facebook'
gem 'omniauth-flickr', '>= 0.0.15'
gem 'omniauth-twitter'
# For charting data
gem 'd3-rails', '~> 3.5' # 4.* produces Error: <spyOn> : could not find an object to spy upon for linear() - see https://travis-ci.org/Growstuff/growstuff/jobs/204461482
gem "chartkick"
# client for Elasticsearch. Elasticsearch is a flexible
# and powerful, distributed, real-time search and analytics engine.

View File

@@ -99,6 +99,7 @@ GEM
capybara-screenshot (1.0.18)
capybara (>= 1.0, < 3)
launchy
chartkick (2.2.5)
childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11)
climate_control (0.2.0)
@@ -143,8 +144,6 @@ GEM
crass (1.0.3)
csv_shaper (1.3.0)
activesupport (>= 3.0.0)
d3-rails (3.5.17)
railties (>= 3.1)
dalli (2.7.6)
database_cleaner (1.6.2)
debug_inspector (0.0.3)
@@ -569,12 +568,12 @@ DEPENDENCIES
capybara
capybara-email
capybara-screenshot
chartkick
codeclimate-test-reporter
coffee-rails
comfortable_mexican_sofa
coveralls
csv_shaper
d3-rails (~> 3.5)
dalli
database_cleaner
devise

View File

@@ -10,6 +10,8 @@
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
// GO AFTER THE REQUIRES BELOW.
//
// = require Chart.bundle
// = require chartkick
// = require leaflet
// = require leaflet.markercluster
// = require js-routes

View File

@@ -1,16 +1,3 @@
//= require graphs/horizontal_bar_graph
if (document.getElementById("cropmap") !== null) {
mapbox_map_id = "<%= Rails.env == 'test' ? 0 : Growstuff::Application.config.mapbox_map_id %>";
mapbox_access_token = "<%= Rails.env == 'test' ? 0 : Growstuff::Application.config.mapbox_access_token %>";
mapbox_base_url = "http://a.tiles.mapbox.com/v4/" + mapbox_map_id + "/{z}/{x}/{y}.png?access_token=" + mapbox_access_token;
L.Icon.Default.imagePath = '/assets'
cropmap = L.map('cropmap').setView([0.0, -0.0], 2);
showCropMap(cropmap);
}
function showCropMap(cropmap) {
L.tileLayer(mapbox_base_url, {
@@ -51,41 +38,20 @@ function showCropMap(cropmap) {
cropmap.addLayer(markers);
}
function plantingStats(crop) {
var sunniness_counts = { 'empty': 0, 'sun': 0, 'semi-shade': 0, 'shade': 0 };
$.each(crop.plantings, function(i, planting) {
if (planting.sunniness) {
sunniness_counts[planting.sunniness]++;
} else {
sunniness_counts['Empty']++;
}
$(document).ready(function() {
if (document.getElementById("cropmap") !== null) {
mapbox_map_id = "<%= Rails.env == 'test' ? 0 : Growstuff::Application.config.mapbox_map_id %>";
mapbox_access_token = "<%= Rails.env == 'test' ? 0 : Growstuff::Application.config.mapbox_access_token %>";
mapbox_base_url = "http://a.tiles.mapbox.com/v4/" + mapbox_map_id + "/{z}/{x}/{y}.png?access_token=" + mapbox_access_token;
L.Icon.Default.imagePath = '/assets'
cropmap = L.map('cropmap').setView([0.0, -0.0], 2);
showCropMap(cropmap);
}
$('.btn.toggle.crop-hierarchy').click(function () {
$('.toggle.crop-hierarchy').toggleClass('hide');
});
return [
{name: 'Empty', value: sunniness_counts['empty']},
{name: 'Sun', value: sunniness_counts['sun']},
{name: 'Semi-shade', value: sunniness_counts['semi-shade']},
{name: 'Shade', value: sunniness_counts['shade']}
];
}
if ($("#sunchart")[0] !== null) {
var HorizontalBarGraph = growstuff.HorizontalBarGraph;
$.getJSON(location.pathname + '.json', function (crop) {
data = {
bars: plantingStats(crop),
bar_color: 'steelblue',
width: {size: 300, scale: 'linear'},
height: {size: 100, scale: 'ordinal'},
//left is used to shift the bars over so that there is
//room for the labels
margin: {top: 0, right: 0, bottom: 0, left: 100}
};
var graph = new HorizontalBarGraph(data);
graph.render(d3.select($('#sunchart')[0]));
});
}
$('.btn.toggle.crop-hierarchy').click(function () {
$('.toggle.crop-hierarchy').toggleClass('hide');
});
});

View File

@@ -1,53 +0,0 @@
// =require d3
// = require graphs/width_scale
// = require graphs/height_scale
/*
* This represents bars for a bar graph.
* Currently these are used for HorizontalBarGraph.
*/
(function() {
'use strict';
var growstuff = (window.growstuff = window.growstuff || {});
var WidthScale = growstuff.WidthScale;
var HeightScale = growstuff.HeightScale;
/**
* data object for bar group
* @param {Object} data Graph configuration
*/
function BarGroup(data) {
this._data = data;
}
BarGroup.prototype.render = function(root) {
var data = this._data;
var bars = this._data.bars;
var widthScale = new WidthScale(data).render();
var heightScale = new HeightScale(data).render();
return root.append('g')
.attr('class', 'bar')
.selectAll('rect')
.data(bars.map(function(bar) {
return bar.value;
}))
.enter()
.append('rect')
.attr('y', function(d, i) {
return heightScale(i);
})
.attr('height', heightScale.rangeBand())
.attr('fill', data.bar_color)
.attr('width', function(d) {
return widthScale(d);
})
.append('title')
.text(function(d) {
return 'This value is ' + d + '.';
});
};
growstuff.BarGroup = BarGroup;
}());

View File

@@ -1,45 +0,0 @@
// =require d3
/**
* This file draws the labels to the left of each bar.
*/
(function() {
'use strict';
var growstuff = (window.growstuff = window.growstuff || {});
/**
* new bar label object
* @param {Object} data Graph configuration
*/
function BarLabelGroup(data) {
this._data = data;
}
BarLabelGroup.prototype.render = function(d3) {
var bars = this._data.bars;
// vvcopy pasta from spike vv -- this is a good candidate for refactor
var barHeight = 40;
return d3.append('g')
.attr('class', 'bar-label')
.selectAll('text')
.data(bars.map(function(bar) {
return bar.name;
}))
.enter()
.append('text')
.attr('x', -80)
.attr('y', function(d, i) {
// shrink the margin between each label to give them an even spread with
// bars
var barLabelSpread = 2/3;
// move them downward to line up with bars
var barLabelTopEdge = 17;
return i * barHeight * (barLabelSpread) + barLabelTopEdge;
})
.text(function(d) {
return d;
});
};
growstuff.BarLabelGroup = BarLabelGroup;
}());

View File

@@ -1,31 +0,0 @@
// =require d3
/**
* Height Scale is used to map the number of bars to the display size of
* the svg
*/
(function() {
'use strict';
var growstuff = (window.growstuff = window.growstuff || {});
/**
* new height scale object
* @param {Object} data Graph configuration
*/
function HeightScale(data) {
this._data = data;
}
HeightScale.prototype.render = function() {
var data = this._data;
var scaleType = data.height.scale;
return d3.scale[scaleType]()
.domain(d3.range(data.bars.length))
.rangeRoundBands([0, data.height.size], 0.05, 0);
};
growstuff.HeightScale = HeightScale;
}());

View File

@@ -1,54 +0,0 @@
// = require d3
// = require graphs/bar_group
// = require graphs/bar_label_group
/**
* Horizontal Bar Graph represents sum total of the graph including all of the parts:
* Bars
* Bar Labels
*
* The main dimensions of the graph are rendered here.
*/
(function() {
'use strict';
var growstuff = (window.growstuff = window.growstuff || {});
var BarGroup = growstuff.BarGroup;
var BarLabelGroup = growstuff.BarLabelGroup;
/**
* create a new graph object
* @param {Object} data Graph configuration
*/
function HorizontalBarGraph(data) {
this._data = data;
this._d3 = d3;
}
HorizontalBarGraph.prototype.render = function(root) {
var width = this._data.width;
var height = this._data.height;
var barLabelGroup = new BarLabelGroup(this._data);
var margin = this._data.margin;
var barGroup = new BarGroup(this._data);
var svg = root
.append('svg')
.attr('width', width.size + margin.left + margin.right)
.attr('height', height.size + margin.top + margin.bottom)
.append('g')
.attr('class', 'bar-graph')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
barGroup.render(svg);
barLabelGroup.render(svg);
return svg;
};
growstuff.HorizontalBarGraph = HorizontalBarGraph;
}());

View File

@@ -1,37 +0,0 @@
// =require d3
/**
* Width scale is used to map the value for the length of each bar
* to the display size of the svg
*/
(function() {
'use strict';
var growstuff = (window.growstuff = window.growstuff || {});
/**
* Object for WidthScale
* @param {Object} data Graph configuration
*/
function WidthScale(data) {
this._data = data;
}
WidthScale.prototype.render = function() {
var data = this._data;
var scaleType = data.width.scale;
var axisSize = data.width.size;
return d3.scale[scaleType]()
.domain([0, this.getMaxValue()])
.range([0, axisSize]);
};
WidthScale.prototype.getMaxValue = function() {
return d3.max(this._data.bars.map(function(bar) {
return bar.value;
}));
};
growstuff.WidthScale = WidthScale;
}());

View File

@@ -5,3 +5,4 @@
@import 'custom_bootstrap/custom_bootstrap'
@import 'overrides'
@import 'graphs'
@import 'predictions'

View File

@@ -13,6 +13,7 @@ $blue: #2f4365
$red: #8e4d43
$orange: #ffa500
$yellow: #b2935c
$white: #ffffff
$body-bg: $beige
$text-color: $brown

View File

@@ -3,37 +3,18 @@
@import "custom_bootstrap/variables"
// this padding needs to be done before the responsive stuff is imported
body
// modifying this for our promotional banner. can be replaced after if
// needed.
// padding-top: $navbar-height + 15px
padding-top: $navbar-height
//@import "bootstrap/responsive"
// Font Awesome
@import "font-awesome-sprockets"
@import "font-awesome"
// Glyphicons
//@import "bootstrap/glyphicons"
.list-inline > li.first
padding-left: 0px
h2
font-size: 150%
//#subtitle
// color: lighten($brown, 30%)
// font-style: italic
// font-weight: normal
// margin-top: 0px
// padding-left: 1em
// padding-top: 0px
h3
font-size: 120%

View File

@@ -0,0 +1,11 @@
.predictions
.metric
height: 180px
border: 1px solid lighten($green, 20%)
background: $white
margin: 4px
strong
font-size: 250%
font-align: center
h3

View File

@@ -51,11 +51,7 @@ class CropsController < ApplicationController
def show
@crop = Crop.includes(:scientific_names, plantings: :photos).find(params[:id])
@posts = @crop.posts.order(created_at: :desc).paginate(page: params[:page])
respond_to do |format|
format.html # show.html.haml
format.json { render json: @crop.to_json(crop_json_fields) }
end
respond_with(@crop)
end
def new
@@ -106,8 +102,30 @@ class CropsController < ApplicationController
respond_with @crop
end
def sunniness
pie_chart_query 'sunniness'
end
def planted_from
pie_chart_query 'planted_from'
end
def harvested_for
@crop = Crop.find(params[:crop_id])
render json: Harvest.joins(:plant_part).where(crop: @crop)
.group("plant_parts.name").count(:id)
end
private
def pie_chart_query(field)
@crop = Crop.find(params[:crop_id])
render json: Planting.where(crop: @crop)
.where.not(field.to_sym => nil)
.where.not(field.to_sym => '')
.group(field.to_sym).count(:id)
end
def notifier
case @crop.approval_status
when "approved"

View File

@@ -1,29 +1,35 @@
- unless crop.perennial.nil?
%p
#{crop.name} is
- if crop.perennial == true
= link_to 'https://en.wikipedia.org/wiki/Annual_vs._perennial_plant_evolution' do
a perennial crop
(living more than two years)
- elsif crop.perennial == false
= link_to 'https://en.wikipedia.org/wiki/Annual_vs._perennial_plant_evolution' do
an annual crop
(living and reproducing in a single year or less)
.predictions
- unless crop.perennial.nil?
.row
.col-md-12
%p
#{crop.name} is
- if crop.perennial == true
= link_to 'https://en.wikipedia.org/wiki/Annual_vs._perennial_plant_evolution' do
a perennial crop
(living more than two years)
- elsif crop.perennial == false
= link_to 'https://en.wikipedia.org/wiki/Annual_vs._perennial_plant_evolution' do
an annual crop
(living and reproducing in a single year or less)
- if crop.annual? && crop.median_lifespan.present?
%p
Median lifespan of #{crop.name} plants is
%strong= crop.median_lifespan
days
.row
- if crop.annual? && crop.median_lifespan.present?
.metric.col-md-3.col-xs-5
%h3 Median lifespan
%strong= crop.median_lifespan
%span days
- if crop.median_days_to_first_harvest.present?
%p
First harvest expected
%strong= crop.median_days_to_first_harvest
days after planting
- if crop.median_days_to_first_harvest.present?
.metric.col-md-3.col-xs-5
%h3 First harvest expected
%strong= crop.median_days_to_first_harvest
%span days
after planting
- if crop.annual? && crop.median_days_to_last_harvest.present?
%p
Last harvest expected
%strong= crop.median_days_to_last_harvest
days after planting
- if crop.annual? && crop.median_days_to_last_harvest.present?
.metric.col-md-3.col-xs-5
%h3 Last harvest expected
%strong= crop.median_days_to_last_harvest
%span days
after planting

View File

@@ -23,37 +23,51 @@
.row
.col-md-9
- if member_signed_in?
= display_seed_availability(@current_member, @crop)
= link_to "View your seeds", seeds_by_owner_path(owner: current_member.slug)
.row
.col-md-12
- if member_signed_in?
= display_seed_availability(@current_member, @crop)
= link_to "View your seeds", seeds_by_owner_path(owner: current_member.slug)
%h2 Predictions
= render 'predictions', crop: @crop
%h2
- if !@crop.plantings.empty?
= @crop.name.titleize
has been planted
= pluralize(@crop.plantings.size, "time")
by #{ENV['GROWSTUFF_SITE_NAME']} members.
- else
Nobody is growing this yet. You could be the first!
%p= render 'crops/photos', crop: @crop
%h2
- if !@crop.plantings.empty?
= @crop.name.titleize
has been planted
= pluralize(@crop.plantings.size, "time")
by #{ENV['GROWSTUFF_SITE_NAME']} members.
- else
Nobody is growing this yet. You could be the first!
.row
.col-md-12
%h2 Predictions
= render 'predictions', crop: @crop
%h2
Sunniness Chart
.row
.col-md-12
%h2 Photos
%p= render 'crops/photos', crop: @crop
.row
.col-md-3
%h2 Sunniness
= pie_chart crop_sunniness_path(@crop), legend: "bottom"
#sunchart
.col-md-3
%h2 Planted from
= pie_chart crop_planted_from_path(@crop), legend: "bottom"
.col-md-3
%h2 Harvested for
= pie_chart crop_harvested_for_path(@crop), legend: "bottom"
%h2
Crop Map
%p
Only plantings by members who have set their locations are shown on this map.
- if current_member && current_member.location.blank?
= link_to "Set your location.", edit_member_registration_path
#cropmap
.row
.col-md-12
%h2 Crop Map
%p
Only plantings by members who have set their locations are shown on this map.
- if current_member && current_member.location.blank?
= link_to "Set your location.", edit_member_registration_path
#cropmap
%a{ name: 'posts' }
%h2 What people are saying about #{@crop.name.pluralize}

View File

@@ -1,5 +1,6 @@
!!! 5
%html{ lang: "en", prefix: "og: http://ogp.me/ns#" }
= javascript_include_tag "application"
= render partial: "layouts/meta"
%body
= render partial: "layouts/header"
@@ -26,6 +27,4 @@
Javascripts
\==================================================
/ Placed at the end of the document so the pages load faster
= javascript_include_tag "application"
!= Growstuff::Application.config.analytics_code

View File

@@ -0,0 +1,14 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join('node_modules')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View File

@@ -50,6 +50,9 @@ Growstuff::Application.routes.draw do
get 'crops/search' => 'crops#search', as: 'crops_search'
resources :crops do
get 'photos' => 'photos#index'
get 'sunniness' => 'crops#sunniness'
get 'planted_from' => 'crops#planted_from'
get 'harvested_for' => 'crops#harvested_for'
end
resources :comments

View File

@@ -181,6 +181,7 @@ feature "crop detail page", js: true do
# 10 days to harvest
FactoryBot.create(:harvest, harvested_at: 190.days.ago, crop: planting.crop,
planting: FactoryBot.create(:planting, planted_at: 200.days.ago, crop: crop))
planting.crop.update_medians
end
it "predicts harvest" do
is_expected.to have_text("First harvest expected 20 days after planting")
@@ -216,7 +217,8 @@ feature "crop detail page", js: true do
end
it "predicts lifespan" do
is_expected.to have_text "Median lifespan of #{crop.name} plants is 99 days"
is_expected.to have_text "Median lifespan"
is_expected.to have_text "99 days"
end
it "describes annual crops" do

View File

@@ -1,54 +0,0 @@
(function() {
'use strict';
/*
These tests are for the BarGroup object.
*/
describe('when drawing the group of bars', function() {
var BarGroup; var subject; var bars; var data;
beforeEach(function() {
BarGroup = growstuff.BarGroup;
bars = [
{name: 'Shade', value: 0.2},
{name: 'Half Shade', value: 0.5},
];
data = {
bars: bars,
bar_color: 'steelblue',
width: {size: 300, scale: 'linear'},
height: {size: 400, scale: 'ordinal'},
};
subject = new BarGroup(data);
subject.render(d3.select('#jasmine_content').append('svg'));
});
it('draws a group', function() {
expect($('g.bar')).toExist();
});
it('draws 2 bars', function() {
expect($('g.bar rect')).toHaveLength(2);
});
it('fills the bars with color', function() {
expect($('g.bar rect')).toHaveAttr('fill', 'steelblue');
});
it('shows a tooltip on hover', function() {
var i;
// get all of the title nodes for the bars
var barNodes = $('g.bar rect title');
for (i = 0; i < bars.length; i++) {
expect(barNodes[i].textContent)
.toBe('This value is ' + bars[i].value + '' + '.');
}
});
});
}());

View File

@@ -1,38 +0,0 @@
(function() {
'use strict';
/*
This file contains tests for the labels that get rendered next to each bar
*/
describe('BarLabelGroup', function() {
var BarLabelGroup; var subject; var data;
beforeEach(function() {
BarLabelGroup = growstuff.BarLabelGroup;
var bars = [
{name: 'Shade', value: 0.2},
{name: 'Half Shade', value: 0.5},
];
data = {
bars: bars,
};
subject = new BarLabelGroup(data);
subject.render(d3.select('#jasmine_content').append('svg'));
});
it('draws a group for labels', function() {
expect($('g.bar-label')).toExist();
});
it('draws 2 bar labels', function() {
expect($('g.bar-label text')).toHaveLength(2);
});
it('has text for 2 bar labels', function() {
// jquery jasmine appends text from all text elements
// into one string
expect($('g.bar-label text')).toHaveText('ShadeHalf Shade');
});
});
}());

View File

@@ -1,35 +0,0 @@
(function() {
'use strict';
/*
Tests for mapping the number of bars to the size of the svg
*/
describe('HeightScale when specifying height', function() {
var data; var bars; var HeightScale; var subject; var mockD3;
beforeEach(function() {
HeightScale = growstuff.HeightScale;
bars = [
{name: 'Shade', value: 0.2},
{name: 'Half Shade', value: 0.5},
];
data = {
bars: bars,
width: {size: 300, scale: 'linear'},
height: {size: 400, scale: 'ordinal'},
};
subject = new HeightScale(data);
mockD3 = jasmine.createSpyObj('d3', ['domain', 'rangeRoundBands']);
mockD3.domain.and.returnValue(mockD3);
mockD3.rangeRoundBands.and.returnValue(mockD3);
spyOn(d3.scale, 'ordinal').and.returnValue(mockD3);
subject.render();
});
it('calls the d3 range round bands function to draw the height', function() {
expect(mockD3.rangeRoundBands).toHaveBeenCalledWith([0, 400], 0.05, 0);
});
});
}());

View File

@@ -1,74 +0,0 @@
(function() {
'use strict';
/*
Tests in this file are for the pieces of HorizontalBarGraph or
are more integration-y type tests that require the full graph.
*/
describe('HorizontalBarGraph', function() {
var BarLabelGroup; var BarGroup; var subject; var data;
beforeEach(function() {
var HorizontalBarGraph = growstuff.HorizontalBarGraph;
var bars = [
{name: 'Shade', value: 0.2},
{name: 'Half Shade', value: 0.5},
];
data = {
bars: bars,
bar_color: 'steelblue',
width: {size: 300, scale: 'linear'},
height: {size: 400, scale: 'ordinal'},
// left is used to shift the bars over so that there is
// room for the labels
margin: {top: 0, right: 0, bottom: 0, left: 100},
};
subject = new HorizontalBarGraph(data);
BarGroup = growstuff.BarGroup;
BarLabelGroup = growstuff.BarLabelGroup;
expect(BarLabelGroup).toExist();
spyOn(BarGroup.prototype, 'render').and.callThrough();
spyOn(BarLabelGroup.prototype, 'render').and.callThrough();
subject.render(d3.select($('#jasmine_content')[0]));
});
it('draws a graph', function() {
expect($('#jasmine_content svg')).toExist();
});
it('draws a group for the whole graph', function() {
expect($('g.bar-graph')).toExist();
});
it('draws a bar group', function() {
expect(BarGroup.prototype.render).toHaveBeenCalled();
});
it('draws a group of bar labels', function() {
expect(BarLabelGroup.prototype.render).toHaveBeenCalled();
});
it('has the expected width and height', function() {
var $svg = $('svg');
var margin = data.margin;
expect($svg).toHaveAttr('width', (data.width.size + margin.left + margin.right) + '');
expect($svg).toHaveAttr('height', (data.height.size + margin.top + margin.bottom) + '');
});
it('draws the graph shifted to the right to accommodate for labels', function() {
expect('g.bar-graph').toHaveAttr('transform', 'translate(100,0)');
});
it('on the x axis, draws at least one bar at max width less margin width', function() {
// because of domain and range mapping
expect('g.bar rect:eq(1)').toHaveAttr('width', '300' );
});
it('on the y axis, all bars are the same height', function() {
expect('g.bar rect:eq(0)').toHaveAttr('height', '195');
expect('g.bar rect:eq(1)').toHaveAttr('height', '195');
});
});
}());

View File

@@ -1,40 +0,0 @@
(function() {
'use strict';
/*
This file contains tests for the mapping the data values to
the length of a bar so that it is the correct size for the screen
*/
describe('GraphScale, when specifying width', function() {
var data; var WidthScale; var subject; var mockD3;
beforeEach(function() {
WidthScale = growstuff.WidthScale;
var bars = [
{name: 'Shade', value: 0.2},
{name: 'Half Shade', value: 0.5},
];
data = {
bars: bars,
width: {size: 300, scale: 'linear'},
height: {size: 400, scale: 'ordinal'},
};
subject = new WidthScale(data, 'width');
mockD3 = jasmine.createSpyObj('d3', ['domain', 'range', 'max']);
mockD3.domain.and.returnValue(mockD3);
mockD3.range.and.returnValue(mockD3);
spyOn(d3.scale, 'linear').and.returnValue(mockD3);
subject.render();
});
it('gets the value of the longest bar', function() {
expect(subject.getMaxValue()).toEqual(0.5);
});
it('calls the d3 range function to draw the width', function() {
expect(mockD3.range).toHaveBeenCalledWith([0, 300]);
});
});
}());