├── .travis.yml ├── README.md ├── app ├── controllers │ └── custom_reports_controller.rb ├── helpers │ └── custom_reports_helper.rb ├── models │ ├── custom_report.rb │ ├── custom_report_series.rb │ └── query_ext.rb └── views │ └── custom_reports │ ├── _form.html.erb │ ├── _links.html.erb │ ├── _series.html.erb │ ├── _series_filters.html.erb │ ├── _sidebar.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb ├── assets ├── javascripts │ ├── custom_report_charts.js │ ├── custom_report_edit.js │ ├── d3.v2.js │ ├── d3.v2.min.js │ ├── nv.d3.js │ └── nv.d3.min.js └── stylesheets │ ├── custom_report.css │ └── nv.d3.css ├── config ├── database-mysql-travis.yml ├── database-postgresql-travis.yml ├── locales │ ├── en.yml │ ├── es.yml │ ├── hu.yml │ ├── pt-br.yml │ ├── ru.yml │ └── tr.yml └── routes.rb ├── db └── migrate │ ├── 20121212125001_create_custom_reports.rb │ ├── 20121212125002_create_custom_report_series.rb │ └── 20121212125003_remove_filters_from_custom_reports.rb ├── init.rb ├── lib ├── redmine_custom_reports.rb └── redmine_custom_reports │ ├── project_patch.rb │ └── user_patch.rb ├── screenshot.png └── test ├── functional └── custom_reports_controller_test.rb └── test_helper.rb /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | services: 4 | - mysql 5 | - postgresql 6 | 7 | rvm: 8 | - 2.2.0 9 | 10 | gemfile: 11 | - $REDMINE_PATH/Gemfile 12 | 13 | env: 14 | - REDMINE_VER=3.1.6 DB=mysql 15 | - REDMINE_VER=3.2.3 DB=mysql 16 | - REDMINE_VER=3.3.0 DB=mysql 17 | 18 | - REDMINE_VER=3.1.6 DB=postgresql 19 | - REDMINE_VER=3.2.3 DB=postgresql 20 | - REDMINE_VER=3.3.0 DB=postgresql 21 | 22 | before_install: 23 | - export PLUGIN_NAME=redmine_custom_reports 24 | - export REDMINE_PATH=$HOME/redmine 25 | - svn co http://svn.redmine.org/redmine/tags/$REDMINE_VER $REDMINE_PATH 26 | - ln -s $TRAVIS_BUILD_DIR $REDMINE_PATH/plugins/$PLUGIN_NAME 27 | - cp config/database-$DB-travis.yml $REDMINE_PATH/config/database.yml 28 | - cd $REDMINE_PATH 29 | 30 | before_script: 31 | - bundle exec rake db:create 32 | - bundle exec rake db:migrate 33 | - bundle exec rake redmine:plugins:migrate 34 | 35 | script: 36 | - bundle exec ruby plugins/$PLUGIN_NAME/test/**/*_test.rb 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine Custom Reports Plugin (with charts) 2 | 3 | [![Build Status](https://travis-ci.org/Restream/redmine_custom_reports.svg?branch=master)](https://travis-ci.org/Restream/redmine_custom_reports) 4 | [![Code Climate](https://codeclimate.com/github/Restream/redmine_custom_reports/badges/gpa.svg)](https://codeclimate.com/github/Restream/redmine_custom_reports) 5 | 6 | Redmine plugin to create project reports using [d3.js](http://d3js.org/) charts (with using [NVD3](http://nvd3.org/)). The data for the report - the number of filtered issues grouped by a column. 7 | 8 | You can use multiple data series. Issues filtered by ordinary redmine filters. 9 | 10 | Tickets are grouped together on the same field, on how tickets can be grouped in a query. Additionally, you can group tickets by text custom fields. In addition to regular columns, you can use custom fields with type "text" to group. 11 | 12 | ## Permissions 13 | 14 | There are public and private custom reports. Permission "View custom reports" allows user to view public and private reports. Permission "Manage custom reports" allows user to manage their (private) reports. And last permission "Manage public custom reports" allows user to manage public reports. 15 | 16 | ## Installing a plugin 17 | 18 | 1. Copy plugin directory into #{RAILS_ROOT}/plugins. 19 | If you are downloading the plugin directly from GitHub, 20 | you can do so by changing into your plugin directory and issuing a command like 21 | 22 | git clone git://github.com/Restream/redmine_custom_reports.git 23 | 24 | 2. Run the following command to upgrade your database (make a db backup before). 25 | 26 | bundle exec rake redmine:plugins:migrate RAILS_ENV=production 27 | 28 | 3. Restart Redmine 29 | 30 | 4. Go to one of your project settings. Click on the Modules tab. 31 | You should see the "Custom reports" module at the end of the modules list. 32 | Enable plugin at project level. Now you will see "Custom report" tab at the project menu. 33 | 34 | ## Screenshot 35 | 36 | ![Sample](screenshot.png) 37 | 38 | ## Compatibility 39 | 40 | This version supports redmine 2.x and 3.x 41 | 42 | For all tested versions see the "tests matrix":https://travis-ci.org/Restream/redmine_custom_reports 43 | -------------------------------------------------------------------------------- /app/controllers/custom_reports_controller.rb: -------------------------------------------------------------------------------- 1 | class CustomReportsController < ApplicationController 2 | unloadable 3 | 4 | before_filter :find_project_by_project_id 5 | before_filter :authorize 6 | before_filter :find_custom_reports, only: [:index, :show, :new, :edit] 7 | before_filter :find_custom_report, only: [:show, :edit, :update, :destroy] 8 | before_filter :authorize_to_manage, only: [:edit, :update, :destroy] 9 | 10 | helper :queries 11 | include QueriesHelper 12 | 13 | def index 14 | end 15 | 16 | def show 17 | end 18 | 19 | def new 20 | @custom_report = @project.custom_reports.build 21 | @custom_report.series.build 22 | end 23 | 24 | def create 25 | params.required(:custom_report).permit! if params.class.method_defined? :required 26 | @custom_report = @project.custom_reports.build(params[:custom_report]) 27 | @custom_report.user = User.current 28 | unless User.current.allowed_to?(:manage_public_custom_reports, @project) || 29 | User.current.admin? 30 | @custom_report.is_public = false 31 | end 32 | 33 | if @custom_report.save 34 | redirect_to url_for( 35 | controller: 'custom_reports', 36 | action: 'show', project_id: @project, id: @custom_report.id), 37 | notice: l(:message_custom_reports_created) 38 | else 39 | render action: 'new' 40 | end 41 | end 42 | 43 | def edit 44 | @custom_report.series.build if @custom_report.series.empty? 45 | end 46 | 47 | def update 48 | unless User.current.allowed_to?(:manage_public_custom_reports, @project) || 49 | User.current.admin? 50 | @custom_report.is_public = false 51 | end 52 | 53 | params.required(:custom_report).permit! if params.class.method_defined? :required 54 | if @custom_report.update_attributes(params[:custom_report]) 55 | redirect_to url_for( 56 | controller: 'custom_reports', 57 | action: 'show', project_id: @project, id: @custom_report.id), 58 | notice: l(:message_custom_reports_updated) 59 | else 60 | render action: 'edit' 61 | end 62 | end 63 | 64 | def destroy 65 | if @custom_report.destroy 66 | flash[:notice] = l(:message_custom_reports_destroyed) 67 | else 68 | flash[:alert] = l(:message_custom_reports_not_destroyed) 69 | end 70 | redirect_to project_custom_reports_url(@project) 71 | end 72 | 73 | private 74 | 75 | def find_custom_reports 76 | @custom_reports = @project.custom_reports.visible.by_name 77 | grouped_reports = @custom_reports.group_by(&:is_public) 78 | @own_custom_reports = grouped_reports[false] 79 | @public_custom_reports = grouped_reports[true] 80 | end 81 | 82 | def find_custom_report 83 | @custom_report = @project.custom_reports.visible.find(params[:id]) 84 | end 85 | 86 | def authorize_to_manage 87 | @custom_report.allowed_to_manage? || deny_access 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /app/helpers/custom_reports_helper.rb: -------------------------------------------------------------------------------- 1 | module CustomReportsHelper 2 | def sanitized_object_name(object_name) 3 | object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, '_').sub(/_$/, '') 4 | end 5 | 6 | def operators_for_select(filter_type) 7 | Query.operators_by_filter_type[filter_type].collect { |o| [l(Query.operators[o]), o] } 8 | end 9 | 10 | def query_options_for_select(query) 11 | options = query.available_filters.collect do |field, options| 12 | unless query.has_filter?(field) 13 | [options[:name] || l(('field_' + field.to_s.gsub(/_id$/, '')).to_sym), field] 14 | end 15 | end 16 | options = [['', '']] + options.compact 17 | options_for_select(options) 18 | end 19 | 20 | def link_to_add_custom_report_series(name, f) 21 | new_object = f.object.series.build 22 | id = new_object.object_id 23 | fields = f.fields_for(:series, new_object, child_index: id) do |builder| 24 | render('series', f: builder) 25 | end 26 | link_to(name, '#', 27 | class: 'add-custom-report-series', 28 | 'data-id' => id, 29 | 'data-fields' => fields.gsub("\n", '') 30 | ) 31 | end 32 | 33 | def width_style_for_series(custom_report) 34 | 'width:100%;' 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/models/custom_report.rb: -------------------------------------------------------------------------------- 1 | class CustomReport < ActiveRecord::Base 2 | unloadable 3 | 4 | CHART_TYPES = %w(undev_pie pie donut bar horizontal_bar stacked_bar) 5 | MULTI_SERIES = %w(horizontal_bar stacked_bar) 6 | 7 | belongs_to :project 8 | belongs_to :user 9 | has_many :series, class_name: 'CustomReportSeries' 10 | 11 | validates_presence_of :project 12 | validates_presence_of :user 13 | validates_presence_of :name 14 | validates_presence_of :group_by 15 | validates_presence_of :null_text 16 | validates_inclusion_of :chart_type, in: CHART_TYPES 17 | 18 | accepts_nested_attributes_for :series, allow_destroy: true 19 | 20 | scope :visible, lambda { |*args| 21 | user = args.shift || User.current 22 | user_id = user.logged? ? user.id : 0 23 | where "(#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id 24 | } 25 | 26 | scope :by_name, -> { order('name') } 27 | 28 | def groupable_columns 29 | QueryExt.new().groupable_columns.select do |col| 30 | if col.respond_to? :custom_field 31 | col.custom_field.is_for_all || 32 | project.all_issue_custom_fields.include?(col.custom_field) 33 | else 34 | true 35 | end 36 | end 37 | end 38 | 39 | def info 40 | { 41 | chart_type: chart_type, 42 | group_by_caption: group_by_column.try(:caption), 43 | series_count: series.count, 44 | multi_series: multi_series? 45 | } 46 | end 47 | 48 | def multi_series? 49 | MULTI_SERIES.include?(chart_type) 50 | end 51 | 52 | def data 53 | if multi_series? 54 | # all series must have the same keys 55 | keys = series.map { |s| s.data_hash.keys }.flatten.uniq 56 | series.map { |s| s.data(keys) } 57 | else 58 | series.map { |s| s.data } 59 | end 60 | end 61 | 62 | def allowed_to_manage?(user = User.current) 63 | user.allowed_to?( 64 | is_public? ? :manage_public_custom_reports : :manage_custom_reports, 65 | project 66 | ) 67 | end 68 | 69 | def group_by_column 70 | groupable_columns.detect { |col| col.name.to_s == group_by } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /app/models/custom_report_series.rb: -------------------------------------------------------------------------------- 1 | class CustomReportSeries < ActiveRecord::Base 2 | unloadable 3 | 4 | serialize :filters 5 | 6 | belongs_to :custom_report, inverse_of: :series 7 | 8 | validates :name, presence: true 9 | 10 | def query 11 | @query ||= build_query 12 | end 13 | 14 | def data(data_keys = []) 15 | _keys = data_keys.dup 16 | _data = { 17 | key: name, 18 | values: data_hash.map do |k, v| 19 | _keys.delete(k) 20 | { label: data_label_text(k), value: v } 21 | end 22 | } 23 | _keys.each do |key| 24 | _data[:values] << { label: data_label_text(key), value: 0 } 25 | end 26 | _data 27 | end 28 | 29 | def data_hash 30 | true 31 | @data_hash ||= (query.result_count_by_group || {}) 32 | end 33 | 34 | def flt=(*args) 35 | filters_hash = args.extract_options! 36 | query.filters = {} 37 | query.add_filters(filters_hash[:f], filters_hash[:op], filters_hash[:v]) 38 | self.filters = query.filters 39 | end 40 | 41 | private 42 | 43 | def build_query 44 | QueryExt.new( 45 | name: name, 46 | filters: filters, 47 | group_by: custom_report.try(:group_by), 48 | project: custom_report.try(:project)) 49 | end 50 | 51 | def data_label_text(label) 52 | (label.present? ? label : custom_report.null_text).to_s 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/models/query_ext.rb: -------------------------------------------------------------------------------- 1 | ISSUE_QUERY_CLASS = Redmine::VERSION.to_s >= '2.3.0' ? IssueQuery : Query 2 | 3 | class QueryExt < ISSUE_QUERY_CLASS 4 | unloadable 5 | 6 | def initialize(*args) 7 | super 8 | available_columns.each do |col| 9 | make_groupable!(col) if groupable_ext?(col) 10 | end 11 | end 12 | 13 | def model_name 14 | superclass.model_name 15 | end 16 | 17 | private 18 | 19 | def groupable_ext?(col) 20 | col.respond_to?(:custom_field) && !col.custom_field.multiple? && 21 | %w(string).include?(col.custom_field.field_format) 22 | end 23 | 24 | def make_groupable!(col) 25 | col.groupable = col.custom_field.order_statement 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/custom_reports/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header_tags do %> 2 | <%= javascript_include_tag 'custom_report_edit.js', plugin: 'redmine_custom_reports' %> 3 | <% end %> 4 | 5 | <%= labelled_form_for custom_report, 6 | url: url, 7 | html: { id: 'custom-report-form' } do |f| %> 8 | 9 | <%= error_messages_for custom_report %> 10 | 11 |
12 | 13 |
14 |

15 | <%= f.text_field :name, size: 80, required: true %> 16 |

17 | 18 |

19 | 20 | <%= text_field 'custom_report', 'description', size: 80 %> 21 |

22 | 23 |

24 | 25 | <%= select 'custom_report', 'chart_type', 26 | CustomReport::CHART_TYPES.collect { |ct| [l("label_chart_type_#{ct}"), ct] }, 27 | include_blank: false %> 28 |

29 | 30 | <% if User.current.admin? || User.current.allowed_to?(:manage_public_custom_reports, @project) %> 31 |

32 | 33 | <%= check_box 'custom_report', 'is_public', 34 | onchange: (User.current.admin? ? nil : 'if (this.checked) {$("custom_report_is_for_all").checked = false; $("custom_report_is_for_all").disabled = true;} else {$("custom_report_is_for_all").disabled = false;}') %> 35 |

36 | <% end %> 37 | 38 |

39 | 40 | <%= select 'custom_report', 'group_by', 41 | @custom_report.groupable_columns.collect { |c| [c.caption, c.name.to_s] }, 42 | include_blank: false %> 43 |

44 | 45 |

46 | 47 | <%= text_field 'custom_report', 'null_text', size: 80 %> 48 |

49 |
50 | 51 | <%= f.fields_for :series do |series_fields| %> 52 | <%= render partial: 'series', locals: { f: series_fields } %> 53 | <% end %> 54 | <%= link_to_add_custom_report_series l(:message_add_custom_report_series), f %> 55 | 56 |
57 | 58 | <%= submit_tag message %> 59 | <% end %> 60 | -------------------------------------------------------------------------------- /app/views/custom_reports/_links.html.erb: -------------------------------------------------------------------------------- 1 | <% if custom_reports && custom_reports.any? %> 2 | <% custom_reports.each do |custom_report| %> 3 | <%= link_to custom_report.name, 4 | { controller: 'custom_reports', 5 | action: 'show', 6 | project_id: @project, 7 | id: custom_report.id } %>
8 | <% end %> 9 | <% else %> 10 | <%= l(:label_no_data) %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /app/views/custom_reports/_series.html.erb: -------------------------------------------------------------------------------- 1 | <% series_id = sanitized_object_name(f.object_name) %> 2 | 3 |
4 | <%= l(:label_series_filters) %> 5 |

6 | <%= f.text_field :name, size: 80, required: true %> 7 |

8 | <%= render partial: 'series_filters', locals: { 9 | f: f, query: f.object.query, series_id: series_id } %> 10 | 11 | <%= link_to l(:message_remove_custom_report_series), '#', 12 | class: 'remove-custom-report-series', 13 | 'data-cant_delete_message' => l(:message_cant_delete_last_series) %> 14 | <%= f.hidden_field :_destroy, value: :false %> 15 |
16 | -------------------------------------------------------------------------------- /app/views/custom_reports/_series_filters.html.erb: -------------------------------------------------------------------------------- 1 | <% filter_attr = "#{f.object_name}[flt]" %> 2 | 3 | 4 | 5 | 107 | 114 | 115 |
6 | 7 | <% query.available_filters.each do |field, options| %> 8 | <% 9 | field_id = "#{series_id}_#{field}" 10 | %> 11 | 12 | id="tr_<%= field_id %>" class="filter" 13 | data-field_id="<%= field_id %>"> 14 | 15 | 25 | 26 | 37 | 38 | 103 | 104 | <% end %> 105 |
16 | <%= check_box_tag "#{filter_attr}f[]", 17 | field, 18 | query.has_filter?(field), 19 | id: "cb_#{field_id}", 20 | class: 'toggle_series_filter' %> 21 | 24 | 27 | <%= label_tag "operators_#{field_id}", 28 | l(:description_filter), 29 | class: 'hidden-for-sighted' %> 30 | <%= select_tag "#{filter_attr}op[#{field}]", 31 | options_for_select( 32 | operators_for_select(options[:type]), 33 | query.operator_for(field)), 34 | id: "operators_#{field_id}", 35 | class: 'toggle_series_filter_operator' %> 36 | 39 | 102 |
106 |
108 | <%= label_tag("#{series_id}_add_filter_select", l(:label_filter_add)) %> 109 | <%= select_tag "#{series_id}_add_filter_select", 110 | query_options_for_select(query), 111 | class: 'add_series_filter', 112 | name: nil %> 113 |
116 | <%= hidden_field_tag "#{filter_attr}f[]", '' %> 117 | -------------------------------------------------------------------------------- /app/views/custom_reports/_sidebar.html.erb: -------------------------------------------------------------------------------- 1 |

<%= l(:label_custom_report_plural) %>

2 | 3 | <%= link_to l(:label_custom_report_view_all), 4 | { controller: 'custom_reports', 5 | action: 'index', 6 | project_id: @project } %>
7 | 8 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 9 | <%= link_to l(:label_custom_report_new), 10 | { controller: 'custom_reports', 11 | action: 'new', 12 | project_id: @project } %>
13 | <% end %> 14 | 15 |

<%= l(:label_custom_report_public_list) %>

16 | <%= render partial: 'links', locals: { custom_reports: @public_custom_reports } %> 17 | 18 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 19 |

<%= l(:label_custom_report_own_list) %>

20 | <%= render partial: 'links', locals: { custom_reports: @own_custom_reports } %> 21 | <% end %> 22 | -------------------------------------------------------------------------------- /app/views/custom_reports/edit.html.erb: -------------------------------------------------------------------------------- 1 |

<%= l(:label_custom_report_edit) %>

2 | 3 | <%= render partial: 'form', 4 | locals: { 5 | custom_report: @custom_report, 6 | message: l(:button_update), 7 | url: url_for(controller: 'custom_reports', action: 'update', id: @custom_report.id) } %> 8 | 9 | <% html_title(l(:label_custom_report_edit)) -%> 10 | 11 | <% content_for :sidebar do %> 12 | <%= render partial: 'sidebar' %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/custom_reports/index.html.erb: -------------------------------------------------------------------------------- 1 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 2 |
3 | <%= link_to(l(:button_create), 4 | new_project_custom_report_path(@project), 5 | class: 'icon icon-add') %> 6 |
7 | <% end %> 8 | 9 |

<%= l(:label_custom_report_plural) %>

10 | 11 | <% if @custom_reports.any? %> 12 |
13 | 14 | 15 | 16 | 17 | 18 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 19 | 20 | <% end %> 21 | 22 | 23 | 24 | <% @custom_reports.each do |custom_report| %> 25 | 26 | 33 | 34 | 35 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 36 | 37 | <% end %> 38 | 51 | 52 | <% end %> 53 | 54 |
<%= l(:field_name) %><%= l(:field_description) %><%= l(:field_chart_type) %><%= l(:field_is_public) %>
27 | <%= link_to custom_report.name, 28 | { controller: 'custom_reports', 29 | action: 'show', 30 | project_id: @project, 31 | id: custom_report.id } %>
32 |
<%= custom_report.description %><%= custom_report.chart_type %><%= custom_report.is_public %> 39 | <% if custom_report.allowed_to_manage? %> 40 | <%= link_to(l(:button_edit), 41 | edit_project_custom_report_path(@project, custom_report), 42 | class: 'icon icon-edit') %> 43 | 44 | <%= link_to(l(:button_delete), 45 | project_custom_report_path(@project, custom_report), 46 | confirm: l(:text_are_you_sure), 47 | method: :delete, 48 | class: 'icon icon-del') %> 49 | <% end %> 50 |
55 |
56 | <% else %> 57 | <%= l(:label_no_data) %> 58 | <% end %> 59 | 60 | 61 | <% content_for :sidebar do %> 62 | <%= render partial: 'sidebar' %> 63 | <% end %> 64 | -------------------------------------------------------------------------------- /app/views/custom_reports/new.html.erb: -------------------------------------------------------------------------------- 1 |

<%= l(:label_custom_report_new) %>

2 | 3 | <%= render partial: 'form', 4 | locals: { 5 | custom_report: @custom_report, 6 | message: l(:button_create), 7 | url: url_for(controller: 'custom_reports', action: 'create') } %> 8 | 9 | <% html_title(l(:label_custom_report_new)) -%> 10 | 11 | <% content_for :sidebar do %> 12 | <%= render partial: 'sidebar' %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/custom_reports/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header_tags do %> 2 | <%= stylesheet_link_tag 'custom_report', plugin: 'redmine_custom_reports' %> 3 | <%= stylesheet_link_tag 'nv.d3.css', plugin: 'redmine_custom_reports' %> 4 | <%= javascript_include_tag 'd3.v2.min.js', plugin: 'redmine_custom_reports' %> 5 | <%= javascript_include_tag 'nv.d3.min.js', plugin: 'redmine_custom_reports' %> 6 | <%= javascript_include_tag 'custom_report_charts', plugin: 'redmine_custom_reports' %> 7 | <% end %> 8 | 9 | <% if @custom_report.allowed_to_manage? %> 10 |
11 | 12 | <%= link_to(l(:button_edit), 13 | edit_project_custom_report_path(@project, @custom_report), 14 | class: 'icon icon-edit') %> 15 | 16 | <%= link_to(l(:button_delete), 17 | project_custom_report_path(@project, @custom_report), 18 | confirm: l(:text_are_you_sure), 19 | method: :delete, 20 | class: 'icon icon-del') %> 21 |
22 | <% end %> 23 | 24 |

<%= h @custom_report.name %>

25 | 26 |

<%= @custom_report.description %>

27 | 28 | <%= content_tag :div, 29 | class: 'custom-report', 30 | 'data-custom_report_info' => @custom_report.info.to_json do %> 31 | <% if @custom_report.multi_series? %> 32 | <% content_tag :div, 33 | class: 'custom-report-chart', 34 | style: width_style_for_series(@custom_report), 35 | 'data-chart_data' => @custom_report.data.to_json do %> 36 | 37 | <% end %> 38 | <% else %> 39 | <% @custom_report.series.each do |series| %> 40 | <%= content_tag :div, 41 | class: 'custom-report-chart', 42 | style: width_style_for_series(@custom_report), 43 | 'data-chart_data' => [series.data].to_json do %> 44 |

<%= series.name %>

45 | 46 | <% end %> 47 | <% end %> 48 | <% end %> 49 | <% end %> 50 | 51 | <% content_for :sidebar do %> 52 | <%= render partial: 'sidebar' %> 53 | <% end %> 54 | -------------------------------------------------------------------------------- /assets/javascripts/custom_report_charts.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function($){ 2 | 3 | function chart() { 4 | function chart(selection) { 5 | selection.each(function() { 6 | 7 | var info = $(this).closest('.custom-report').data().custom_report_info; 8 | var data = $(this).data().chart_data; 9 | var svg = d3.select(this).select("svg"); 10 | 11 | switch (info.chart_type) { 12 | 13 | case "undev_pie": 14 | 15 | nv.addGraph(function() { 16 | var chart = nv.models.undevPieChart() 17 | .x(function(d) { return d.label }) 18 | .y(function(d) { return d.value }) 19 | .showLabels(true); 20 | 21 | svg.datum(data) 22 | .transition() 23 | .duration(1200) 24 | .call(chart); 25 | 26 | nv.utils.windowResize(chart.update); 27 | 28 | return chart; 29 | }); 30 | 31 | break; 32 | 33 | case "pie": 34 | 35 | nv.addGraph(function() { 36 | var chart = nv.models.pieChart() 37 | .x(function(d) { return d.label }) 38 | .y(function(d) { return d.value }) 39 | .showLabels(true); 40 | 41 | svg.datum(data) 42 | .transition() 43 | .duration(1200) 44 | .call(chart); 45 | 46 | nv.utils.windowResize(chart.update); 47 | 48 | return chart; 49 | }); 50 | 51 | break; 52 | 53 | case "donut": 54 | nv.addGraph(function() { 55 | var chart = nv.models.pieChart() 56 | .x(function(d) { return d.label }) 57 | .y(function(d) { return d.value }) 58 | .showLabels(false) 59 | .donut(true); 60 | 61 | svg.datum(data) 62 | .transition() 63 | .duration(1200) 64 | .call(chart); 65 | 66 | nv.utils.windowResize(chart.update); 67 | 68 | return chart; 69 | }); 70 | break; 71 | 72 | case "horizontal_bar": 73 | nv.addGraph(function() { 74 | var chart = nv.models.multiBarHorizontalChart() 75 | .x(function(d) { return d.label }) 76 | .y(function(d) { return d.value }) 77 | .margin({top: 30, right: 20, bottom: 50, left: 175}) 78 | .showValues(true) 79 | .tooltips(false) 80 | .showControls(false); 81 | 82 | chart.yAxis 83 | .tickFormat(d3 84 | .format(',.2f')); 85 | 86 | svg.datum(data) 87 | .transition() 88 | .duration(500) 89 | .call(chart); 90 | 91 | nv.utils.windowResize(chart.update); 92 | 93 | return chart; 94 | }); 95 | break; 96 | 97 | case "bar": 98 | nv.addGraph(function() { 99 | var chart = nv.models.discreteBarChart() 100 | .x(function(d) { return d.label }) 101 | .y(function(d) { return d.value }) 102 | .staggerLabels(true) 103 | .tooltips(false) 104 | .showValues(true); 105 | 106 | svg.datum(data) 107 | .transition() 108 | .duration(500) 109 | .call(chart); 110 | 111 | nv.utils.windowResize(chart.update); 112 | 113 | return chart; 114 | }); 115 | break; 116 | 117 | case "stacked_bar": 118 | nv.addGraph(function() { 119 | var chart = nv.models.multiBarChart() 120 | .x(function(d) { return d.label }) 121 | .y(function(d) { return d.value }); 122 | 123 | chart.xAxis 124 | .axisLabel(info.group_by_caption); 125 | 126 | svg.datum(data) 127 | .transition() 128 | .duration(500) 129 | .call(chart); 130 | 131 | nv.utils.windowResize(chart.update); 132 | 133 | return chart; 134 | }); 135 | break; 136 | 137 | default: 138 | throw "unknown chart type " + chartType; 139 | } 140 | }) 141 | } 142 | return chart; 143 | } 144 | 145 | $('.custom-report').each(function() { 146 | var info = $(this).data().custom_report_info; 147 | if (info.multi_series) { 148 | var report_height = document.body.clientHeight * 0.8; 149 | $(this).children('.custom-report-chart').css('height', report_height); 150 | } 151 | }); 152 | 153 | d3.selectAll(".custom-report-chart").call(chart()); 154 | 155 | }); 156 | -------------------------------------------------------------------------------- /assets/javascripts/custom_report_edit.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function($) { 2 | 3 | function findSeriesId(el){ 4 | var dataEl = $(el).closest('[data-series_id]'); 5 | return dataEl.data().series_id; 6 | } 7 | 8 | function findFieldId(el){ 9 | var dataEl = $(el).closest('[data-field_id]'); 10 | return dataEl.data().field_id; 11 | } 12 | 13 | function toggleSeriesFilter(field_id) { 14 | var check_box = $('#cb_' + field_id); 15 | var operators = $('#operators_' + field_id); 16 | if (check_box.is(':checked')) { 17 | operators.show(); 18 | operators.removeAttr('disabled'); 19 | toggleSeriesFilterOperator(field_id); 20 | } else { 21 | operators.hide(); 22 | operators.attr('disabled', 'disabled'); 23 | enableSeriesFilterValues(field_id, []); 24 | } 25 | return 'todo'; 26 | } 27 | 28 | function addSeriesFilter() { 29 | var selectEl = $(this); 30 | var field = selectEl.val(); 31 | var series_id = findSeriesId(this); 32 | var field_id = series_id + '_' + field; 33 | var check_box = $('#cb_' + field_id); 34 | 35 | $('#tr_' + field_id).show(); 36 | check_box.attr('checked', 'checked'); 37 | toggleSeriesFilter(field_id); 38 | selectEl.val(''); 39 | selectEl.children('option[value=' + field + ']').attr('disabled', 'disabled'); 40 | } 41 | 42 | function toggleSeriesFilterOperator(field_id) { 43 | var operator = $('#operators_' + field_id); 44 | switch (operator.val()) { 45 | case '!*': 46 | case '*': 47 | case 't': 48 | case 'w': 49 | case 'o': 50 | case 'c': 51 | enableSeriesFilterValues(field_id, []); 52 | break; 53 | case '><': 54 | enableSeriesFilterValues(field_id, [0,1]); 55 | break; 56 | case 't+': 58 | case 't+': 59 | case '>t-': 60 | case '= 0) { 75 | value.removeAttr('disabled'); 76 | value.closest('span').show(); 77 | } else { 78 | value.attr('disabled', 'disabled'); 79 | value.closest('span').hide(); 80 | } 81 | } 82 | if (indexes.length > 0) { 83 | $('#div_values_' + field_id).show(); 84 | } else { 85 | $('#div_values_' + field_id).hide(); 86 | } 87 | } 88 | 89 | $('body').on('click', '.remove-custom-report-series', function(event){ 90 | var series_count = fieldset = $('fieldset[data-series_id]').length; 91 | if (series_count > 1) { 92 | var series_id = findSeriesId(this); 93 | var fieldset = $('fieldset[data-series_id=' + series_id + ']'); 94 | var destroy_field = fieldset.children('#' + series_id + '__destroy'); 95 | destroy_field.val("true"); 96 | fieldset.hide(); 97 | } else { 98 | var cant_delete_message = $(this).data().cant_delete_message; 99 | alert(cant_delete_message); 100 | } 101 | event.preventDefault(); 102 | }); 103 | 104 | $('body').on('click', '.add-custom-report-series', function(event){ 105 | var id = $(this).data().id; 106 | var fields = $(this).data().fields; 107 | var time = new Date().getTime(); 108 | var regexp = new RegExp(id, 'g'); 109 | $(this).before(fields.replace(regexp, time)); 110 | event.preventDefault(); 111 | }); 112 | 113 | $('body').on('change', '.toggle_series_filter', function(){ 114 | var field_id = findFieldId(this); 115 | toggleSeriesFilter(field_id); 116 | }); 117 | 118 | $('body').on('change', '.toggle_series_filter_operator', function(){ 119 | var field_id = findFieldId(this); 120 | toggleSeriesFilterOperator(field_id); 121 | }); 122 | 123 | $('body').on('change', '.add_series_filter', addSeriesFilter); 124 | 125 | $('[data-field_id]').each(function(){ 126 | var field_id = $(this).data().field_id; 127 | toggleSeriesFilterOperator(field_id); 128 | }); 129 | 130 | }); 131 | 132 | function toggle_multi_select(id) { 133 | var select = $('#'+id); 134 | if (select.attr('multiple')) { 135 | select.removeAttr('multiple'); 136 | } else { 137 | select.attr('multiple', true); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /assets/javascripts/d3.v2.min.js: -------------------------------------------------------------------------------- 1 | (function(){function e(e,t){try{for(var n in t)Object.defineProperty(e.prototype,n,{value:t[n],enumerable:!1})}catch(r){e.prototype=t}}function t(e){var t=-1,n=e.length,r=[];while(++t=0?e.substring(t):(t=e.length,""),r=[];while(t>0)r.push(e.substring(t-=3,t+3));return r.reverse().join(",")+n}function b(e,t){var n=Math.pow(10,Math.abs(8-t)*3);return{scale:t>8?function(e){return e/n}:function(e){return e*n},symbol:e}}function w(e){return function(t){return t<=0?0:t>=1?1:e(t)}}function E(e){return function(t){return 1-e(1-t)}}function S(e){return function(t){return.5*(t<.5?e(2*t):2-e(2-2*t))}}function x(e){return e}function T(e){return function(t){return Math.pow(t,e)}}function N(e){return 1-Math.cos(e*Math.PI/2)}function C(e){return Math.pow(2,10*(e-1))}function k(e){return 1-Math.sqrt(1-e*e)}function L(e,t){var n;return arguments.length<2&&(t=.45),arguments.length<1?(e=1,n=t/4):n=t/(2*Math.PI)*Math.asin(1/e),function(r){return 1+e*Math.pow(2,10*-r)*Math.sin((r-n)*2*Math.PI/t)}}function A(e){return e||(e=1.70158),function(t){return t*t*((e+1)*t-e)}}function O(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375}function M(){d3.event.stopPropagation(),d3.event.preventDefault()}function _(){var e=d3.event,t;while(t=e.sourceEvent)e=t;return e}function D(e){var t=new d,n=0,r=arguments.length;while(++n360?e-=360:e<0&&(e+=360),e<60?s+(o-s)*e/60:e<180?o:e<240?s+(o-s)*(240-e)/60:s}function i(e){return Math.round(r(e)*255)}var s,o;return e%=360,e<0&&(e+=360),t=t<0?0:t>1?1:t,n=n<0?0:n>1?1:n,o=n<=.5?n*(1+t):n+t-n*t,s=2*n-o,U(i(e+120),i(e),i(e-120))}function Z(e,t,n){return new et(e,t,n)}function et(e,t,n){this.h=e,this.c=t,this.l=n}function tt(e,t,n){return nt(n,Math.cos(e*=Math.PI/180)*t,Math.sin(e)*t)}function nt(e,t,n){return new rt(e,t,n)}function rt(e,t,n){this.l=e,this.a=t,this.b=n}function it(e,t,n){var r=(e+16)/116,i=r+t/500,s=r-n/200;return i=ot(i)*ys,r=ot(r)*bs,s=ot(s)*ws,U(at(3.2404542*i-1.5371385*r-.4985314*s),at(-0.969266*i+1.8760108*r+.041556*s),at(.0556434*i-.2040259*r+1.0572252*s))}function st(e,t,n){return Z(Math.atan2(n,t)/Math.PI*180,Math.sqrt(t*t+n*n),e)}function ot(e){return e>.206893034?e*e*e:(e-4/29)/7.787037}function ut(e){return e>.008856?Math.pow(e,1/3):7.787037*e+4/29}function at(e){return Math.round(255*(e<=.00304?12.92*e:1.055*Math.pow(e,1/2.4)-.055))}function ft(e){return Qi(e,ks),e}function lt(e){return function(){return Ss(e,this)}}function ct(e){return function(){return xs(e,this)}}function ht(e,t){function n(){this.removeAttribute(e)}function r(){this.removeAttributeNS(e.space,e.local)}function i(){this.setAttribute(e,t)}function s(){this.setAttributeNS(e.space,e.local,t)}function o(){var n=t.apply(this,arguments);n==null?this.removeAttribute(e):this.setAttribute(e,n)}function u(){var n=t.apply(this,arguments);n==null?this.removeAttributeNS(e.space,e.local):this.setAttributeNS(e.space,e.local,n)}return e=d3.ns.qualify(e),t==null?e.local?r:n:typeof t=="function"?e.local?u:o:e.local?s:i}function pt(e){return new RegExp("(?:^|\\s+)"+d3.requote(e)+"(?:\\s+|$)","g")}function dt(e,t){function n(){var n=-1;while(++n0&&(e=e.substring(0,o)),t?i:r}function St(e,t){for(var n=0,r=e.length;nt?c():(v.active=t,i.forEach(function(t,n){(n=n.call(e,m,u))&&h.push(n)}),s.start.call(e,m,u),l(r)||d3.timer(l,0,n),1)}function l(n){if(v.active!==t)return c();var r=(n-p)/d,i=o(r),a=h.length;while(a>0)h[--a].call(e,i);if(r>=1)return c(),_s=t,s.end.call(e,m,u),_s=0,1}function c(){return--v.count||delete e.__transition__,1}var h=[],p=e.delay,d=e.duration,v=(e=e.node).__transition__||(e.__transition__={active:0,count:0}),m=e.__data__;++v.count,p<=r?f(r):d3.timer(f,p,n)})},0,n),e}function Nt(e){var t=_s,n=Fs,r=Bs,i=js;return _s=this.id,Fs=this.ease(),St(this,function(t,n,r){Bs=t.delay,js=t.duration,e.call(t=t.node,t.__data__,n,r)}),_s=t,Fs=n,Bs=r,js=i,this}function Ct(e,t,n){return n!=""&&Is}function kt(e,t){return d3.tween(e,F(t))}function Lt(){var e,t=Date.now(),n=Us;while(n)e=t-n.then,e>=n.delay&&(n.flush=n.callback(e)),n=n.next;var r=At()-t;r>24?(isFinite(r)&&(clearTimeout(Ws),Ws=setTimeout(Lt,r)),zs=0):(zs=1,Xs(Lt))}function At(){var e=null,t=Us,n=Infinity;while(t)t.flush?(delete Rs[t.callback.id],t=e?e.next=t.next:Us=t.next):(n=Math.min(n,t.then+t.delay),t=(e=t).next);return n}function Ot(e,t){var n=e.ownerSVGElement||e;if(n.createSVGPoint){var r=n.createSVGPoint();if(Vs<0&&(window.scrollX||window.scrollY)){n=d3.select(document.body).append("svg").style("position","absolute").style("top",0).style("left",0);var i=n[0][0].getScreenCTM();Vs=!i.f&&!i.e,n.remove()}return Vs?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(e.getScreenCTM().inverse()),[r.x,r.y]}var s=e.getBoundingClientRect();return[t.clientX-s.left-e.clientLeft,t.clientY-s.top-e.clientTop]}function Mt(){}function _t(e){var t=e[0],n=e[e.length-1];return t2?zt:Ut,a=r?q:I;return o=i(e,t,a,n),u=i(t,e,a,d3.interpolate),s}function s(e){return o(e)}var o,u;return s.invert=function(e){return u(e)},s.domain=function(t){return arguments.length?(e=t.map(Number),i()):e},s.range=function(e){return arguments.length?(t=e,i()):t},s.rangeRound=function(e){return s.range(e).interpolate(d3.interpolateRound)},s.clamp=function(e){return arguments.length?(r=e,i()):r},s.interpolate=function(e){return arguments.length?(n=e,i()):n},s.ticks=function(t){return qt(e,t)},s.tickFormat=function(t){return Rt(e,t)},s.nice=function(){return Pt(e,Ft),i()},s.copy=function(){return Bt(e,t,n,r)},i()}function jt(e,t){return d3.rebind(e,t,"range","rangeRound","interpolate","clamp")}function Ft(e){return e=Math.pow(10,Math.round(Math.log(e)/Math.LN10)-1),e&&{floor:function(t){return Math.floor(t/e)*e},ceil:function(t){return Math.ceil(t/e)*e}}}function It(e,t){var n=_t(e),r=n[1]-n[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),s=t/r*i;return s<=.15?i*=10:s<=.35?i*=5:s<=.75&&(i*=2),n[0]=Math.ceil(n[0]/i)*i,n[1]=Math.floor(n[1]/i)*i+i*.5,n[2]=i,n}function qt(e,t){return d3.range.apply(d3,It(e,t))}function Rt(e,t){return d3.format(",."+Math.max(0,-Math.floor(Math.log(It(e,t)[2])/Math.LN10+.01))+"f")}function Ut(e,t,n,r){var i=n(e[0],e[1]),s=r(t[0],t[1]);return function(e){return s(i(e))}}function zt(e,t,n,r){var i=[],s=[],o=0,u=Math.min(e.length,t.length)-1;e[u]0;f--)i.push(r(s)*f)}else{for(;sa;o--);i=i.slice(s,o)}return i},n.tickFormat=function(e,i){arguments.length<2&&(i=$s);if(arguments.length<1)return i;var s=Math.max(.1,e/n.ticks().length),o=t===Vt?(u=-1e-12,Math.floor):(u=1e-12,Math.ceil),u;return function(e){return e/r(o(t(e)+u))<=s?i(e):""}},n.copy=function(){return Wt(e.copy(),t)},jt(n,e)}function Xt(e){return Math.log(e<0?0:e)/Math.LN10}function Vt(e){return-Math.log(e>0?0:-e)/Math.LN10}function $t(e,t){function n(t){return e(r(t))}var r=Jt(t),i=Jt(1/t);return n.invert=function(t){return i(e.invert(t))},n.domain=function(t){return arguments.length?(e.domain(t.map(r)),n):e.domain().map(i)},n.ticks=function(e){return qt(n.domain(),e)},n.tickFormat=function(e){return Rt(n.domain(),e)},n.nice=function(){return n.domain(Pt(n.domain(),Ft))},n.exponent=function(e){if(!arguments.length)return t;var s=n.domain();return r=Jt(t=e),i=Jt(1/t),n.domain(s)},n.copy=function(){return $t(e.copy(),t)},jt(n,e)}function Jt(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function Kt(e,t){function n(t){return o[((s.get(t)||s.set(t,e.push(t)))-1)%o.length]}function i(t,n){return d3.range(e.length).map(function(e){return t+n*e})}var s,o,u;return n.domain=function(i){if(!arguments.length)return e;e=[],s=new r;var o=-1,u=i.length,a;while(++o1){u=t[1],s=e[a],a++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(s[0]-u[0])+","+(s[1]-u[1])+","+s[0]+","+s[1];for(var f=2;f9&&(s=n*3/Math.sqrt(s),o[u]=s*r,o[u+1]=s*i));u=-1;while(++u<=a)s=(e[Math.min(a,u+1)][0]-e[Math.max(0,u-1)][0])/(6*(1+o[u]*o[u])),t.push([s||0,o[u]*s||0]);return t}function Cn(e){return e.length<3?an(e):e[0]+vn(e,Nn(e))}function kn(e){var t,n=-1,r=e.length,i,s;while(++n1){var r=_t(e.domain()),i,s=-1,o=t.length,u=(t[1]-t[0])/++n,a,f;while(++s0;)(f=+t[s]-a*u)>=r[0]&&i.push(f);for(--s,a=0;++ar&&(n=t,r=i);return n}function sr(e){return e.reduce(or,0)}function or(e,t){return e+t[1]}function ur(e,t){return ar(e,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ar(e,t){var n=-1,r=+e[0],i=(e[1]-r)/t,s=[];while(++n<=t)s[n]=i*n+r;return s}function fr(e){return[d3.min(e),d3.max(e)]}function lr(e,t){return d3.rebind(e,t,"sort","children","value"),e.links=dr,e.nodes=function(t){return vo=!0,(e.nodes=e)(t)},e}function cr(e){return e.children}function hr(e){return e.value}function pr(e,t){return t.value-e.value}function dr(e){return d3.merge(e.map(function(e){return(e.children||[]).map(function(t){return{source:e,target:t}})}))}function vr(e,t){return e.value-t.value}function mr(e,t){var n=e._pack_next;e._pack_next=t,t._pack_prev=e,t._pack_next=n,n._pack_prev=t}function gr(e,t){e._pack_next=t,t._pack_prev=e}function yr(e,t){var n=t.x-e.x,r=t.y-e.y,i=e.r+t.r;return i*i-n*n-r*r>.001}function br(e){function t(e){r=Math.min(e.x-e.r,r),i=Math.max(e.x+e.r,i),s=Math.min(e.y-e.r,s),o=Math.max(e.y+e.r,o)}if(!(n=e.children)||!(p=n.length))return;var n,r=Infinity,i=-Infinity,s=Infinity,o=-Infinity,u,a,f,l,c,h,p;n.forEach(wr),u=n[0],u.x=-u.r,u.y=0,t(u);if(p>1){a=n[1],a.x=a.r,a.y=0,t(a);if(p>2){f=n[2],xr(u,a,f),t(f),mr(u,f),u._pack_prev=f,mr(f,a),a=u._pack_next;for(l=3;l0&&(e=r)}return e}function _r(e,t){return e.x-t.x}function Dr(e,t){return t.x-e.x}function Pr(e,t){return e.depth-t.depth}function Hr(e,t){function n(e,r){var i=e.children;if(i&&(a=i.length)){var s,o=null,u=-1,a;while(++u=0)s=r[i]._tree,s.prelim+=t,s.mod+=t,t+=s.shift+(n+=s.change)}function jr(e,t,n){e=e._tree,t=t._tree;var r=n/(t.number-e.number);e.change+=r,t.change-=r,t.shift+=n,t.prelim+=n,t.mod+=n}function Fr(e,t,n){return e._tree.ancestor.parent==t.parent?e._tree.ancestor:n}function Ir(e){return{x:e.x,y:e.y,dx:e.dx,dy:e.dy}}function qr(e,t){var n=e.x+t[3],r=e.y+t[0],i=e.dx-t[1]-t[3],s=e.dy-t[0]-t[2];return i<0&&(n+=i/2,i=0),s<0&&(r+=s/2,s=0),{x:n,y:r,dx:i,dy:s}}function Rr(e,t){function n(e,r){d3.text(e,t,function(e){r(e&&n.parse(e))})}function r(t){return t.map(i).join(e)}function i(e){return o.test(e)?'"'+e.replace(/\"/g,'""')+'"':e}var s=new RegExp("\r\n|["+e+"\r\n]","g"),o=new RegExp('["'+e+"\n]"),u=e.charCodeAt(0);return n.parse=function(e){var t;return n.parseRows(e,function(e,n){if(n){var r={},i=-1,s=t.length;while(++i=e.length)return i;if(l)return l=!1,r;var t=s.lastIndex;if(e.charCodeAt(t)===34){var n=t;while(n++0}function si(e,t,n){return(n[0]-t[0])*(e[1]-t[1])<(n[1]-t[1])*(e[0]-t[0])}function oi(e,t,n,r){var i=e[0],s=t[0],o=n[0],u=r[0],a=e[1],f=t[1],l=n[1],c=r[1],h=i-o,p=s-i,d=u-o,v=a-l,m=f-a,g=c-l,y=(d*v-g*h)/(g*p-d*m);return[i+y*p,a+y*m]}function ui(e,t){var n={list:e.map(function(e,t){return{index:t,x:e[0],y:e[1]}}).sort(function(e,t){return e.yt.y?1:e.xt.x?1:0}),bottomSite:null},r={list:[],leftEnd:null,rightEnd:null,init:function(){r.leftEnd=r.createHalfEdge(null,"l"),r.rightEnd=r.createHalfEdge(null,"l"),r.leftEnd.r=r.rightEnd,r.rightEnd.l=r.leftEnd,r.list.unshift(r.leftEnd,r.rightEnd)},createHalfEdge:function(e,t){return{edge:e,side:t,vertex:null,l:null,r:null}},insert:function(e,t){t.l=e,t.r=e.r,e.r.l=t,e.r=t},leftBound:function(e){var t=r.leftEnd;do t=t.r;while(t!=r.rightEnd&&i.rightOf(t,e));return t=t.l,t},del:function(e){e.l.r=e.r,e.r.l=e.l,e.edge=null},right:function(e){return e.r},left:function(e){return e.l},leftRegion:function(e){return e.edge==null?n.bottomSite:e.edge.region[e.side]},rightRegion:function(e){return e.edge==null?n.bottomSite:e.edge.region[wo[e.side]]}},i={bisect:function(e,t){var n={region:{l:e,r:t},ep:{l:null,r:null}},r=t.x-e.x,i=t.y-e.y,s=r>0?r:-r,o=i>0?i:-i;return n.c=e.x*r+e.y*i+(r*r+i*i)*.5,s>o?(n.a=1,n.b=i/r,n.c/=r):(n.b=1,n.a=r/i,n.c/=i),n},intersect:function(e,t){var n=e.edge,r=t.edge;if(!n||!r||n.region.r==r.region.r)return null;var i=n.a*r.b-n.b*r.a;if(Math.abs(i)<1e-10)return null;var s=(n.c*r.b-r.c*n.b)/i,o=(r.c*n.a-n.c*r.a)/i,u=n.region.r,a=r.region.r,f,l;u.y=l.region.r.x;return c&&f.side==="l"||!c&&f.side==="r"?null:{x:s,y:o}},rightOf:function(e,t){var n=e.edge,r=n.region.r,i=t.x>r.x;if(i&&e.side==="l")return 1;if(!i&&e.side==="r")return 0;if(n.a===1){var s=t.y-r.y,o=t.x-r.x,u=0,a=0;!i&&n.b<0||i&&n.b>=0?a=u=s>=n.b*o:(a=t.x+t.y*n.b>n.c,n.b<0&&(a=!a),a||(u=1));if(!u){var f=r.x-n.region.l.x;a=n.b*(o*o-s*s)h*h+p*p}return e.side==="l"?a:!a},endPoint:function(e,n,r){e.ep[n]=r;if(!e.ep[wo[n]])return;t(e)},distance:function(e,t){var n=e.x-t.x,r=e.y-t.y;return Math.sqrt(n*n+r*r)}},s={list:[],insert:function(e,t,n){e.vertex=t,e.ystar=t.y+n;for(var r=0,i=s.list,o=i.length;ru.ystar||e.ystar==u.ystar&&t.x>u.vertex.x)continue;break}i.splice(r,0,e)},del:function(e){for(var t=0,n=s.list,r=n.length;td.y&&(v=p,p=d,d=v,b="r"),y=i.bisect(p,d),h=r.createHalfEdge(y,b),r.insert(l,h),i.endPoint(y,wo[b],g),m=i.intersect(l,h),m&&(s.del(l),s.insert(l,m,i.distance(m,p))),m=i.intersect(h,c),m&&s.insert(h,m,i.distance(m,p))}}for(a=r.right(r.leftEnd);a!=r.rightEnd;a=r.right(a))t(a.edge)}function ai(){return{leaf:!0,nodes:[],point:null}}function fi(e,t,n,r,i,s){if(!e(t,n,r,i,s)){var o=(n+i)*.5,u=(r+s)*.5,a=t.nodes;a[0]&&fi(e,a[0],n,r,o,u),a[1]&&fi(e,a[1],o,r,i,u),a[2]&&fi(e,a[2],n,u,o,s),a[3]&&fi(e,a[3],o,u,i,s)}}function li(e){return{x:e[0],y:e[1]}}function ci(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function hi(e){return e.substring(0,3)}function pi(e,t,n,r){var i,s,o=0,u=t.length,a=n.length;while(o=a)return-1;i=t.charCodeAt(o++);if(i==37){s=Uo[t.charAt(o++)];if(!s||(r=s(e,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}function di(e){return new RegExp("^(?:"+e.map(d3.requote).join("|")+")","i")}function vi(e){var t=new r,n=-1,i=e.length;while(++n68?1900:2e3)}function Ci(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.m=r[0]-1,n+=r[0].length):-1}function ki(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.d=+r[0],n+=r[0].length):-1}function Li(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.H=+r[0],n+=r[0].length):-1}function Ai(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.M=+r[0],n+=r[0].length):-1}function Oi(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.S=+r[0],n+=r[0].length):-1}function Mi(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+3));return r?(e.L=+r[0],n+=r[0].length):-1}function _i(e,t,n){var r=Wo.get(t.substring(n,n+=2).toLowerCase());return r==null?-1:(e.p=r,n)}function Di(e){var t=e.getTimezoneOffset(),n=t>0?"-":"+",r=~~(Math.abs(t)/60),i=Math.abs(t)%60;return n+Mo(r)+Mo(i)}function Pi(e){return e.toISOString()}function Hi(e,t,n){function r(t){var n=e(t),r=s(n,1);return t-n1)while(ot?1:e>=t?0:NaN},d3.descending=function(e,t){return te?1:t>=e?0:NaN},d3.mean=function(e,t){var n=e.length,r,i=0,s=-1,o=0;if(arguments.length===1)while(++s1&&(e=e.map(t)),e=e.filter(f),e.length?d3.quantile(e.sort(d3.ascending),.5):undefined},d3.min=function(e,t){var n=-1,r=e.length,i,s;if(arguments.length===1){while(++ns&&(i=s)}else{while(++ns&&(i=s)}return i},d3.max=function(e,t){var n=-1,r=e.length,i,s;if(arguments.length===1){while(++ni&&(i=s)}else{while(++ni&&(i=s)}return i},d3.extent=function(e,t){var n=-1,r=e.length,i,s,o;if(arguments.length===1){while(++ns&&(i=s),os&&(i=s),o1);return e+t*n*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(e,t){var n=arguments.length;n<2&&(t=1),n<1&&(e=0);var r=d3.random.normal();return function(){return Math.exp(e+t*r())}},irwinHall:function(e){return function(){for(var t=0,n=0;n>>1;e.call(t,t[s],s)>>1;n0&&(i=s);return i},d3.last=function(e,t){var n=0,r=e.length,i=e[0],s;arguments.length===1&&(t=d3.ascending);while(++n=i.length)return u?u.call(n,t):o?t.sort(o):t;var a=-1,f=t.length,l=i[s++],c,h,p=new r,d,v={};while(++a=i.length)return e;var r=[],o=s[n++],u;for(u in e)r.push({key:u,values:t(e[u],n)});return o&&r.sort(function(e,t){return o(e.key,t.key)}),r}var n={},i=[],s=[],o,u;return n.map=function(t){return e(t,0)},n.entries=function(n){return t(e(n,0),0)},n.key=function(e){return i.push(e),n},n.sortKeys=function(e){return s[i.length-1]=e,n},n.sortValues=function(e){return o=e,n},n.rollup=function(e){return u=e,n},n},d3.keys=function(e){var t=[];for(var n in e)t.push(n);return t},d3.values=function(e){var t=[];for(var n in e)t.push(e[n]);return t},d3.entries=function(e){var t=[];for(var n in e)t.push({key:n,value:e[n]});return t},d3.permute=function(e,t){var n=[],r=-1,i=t.length;while(++rt)r.push(o/i);else while((o=e+n*++s)=200&&e<300||e===304?r:null)}},r.send(null)},d3.text=function(e,t,n){function r(e){n(e&&e.responseText)}arguments.length<3&&(n=t,t=null),d3.xhr(e,t,r)},d3.json=function(e,t){d3.text(e,"application/json",function(e){t(e?JSON.parse(e):null)})},d3.html=function(e,t){d3.text(e,"text/html",function(e){if(e!=null){var n=document.createRange();n.selectNode(document.body),e=n.createContextualFragment(e)}t(e)})},d3.xml=function(e,t,n){function r(e){n(e&&e.responseXML)}arguments.length<3&&(n=t,t=null),d3.xhr(e,t,r)};var ts={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};d3.ns={prefix:ts,qualify:function(e){var t=e.indexOf(":"),n=e;return t>=0&&(n=e.substring(0,t),e=e.substring(t+1)),ts.hasOwnProperty(n)?{space:ts[n],local:e}:e}},d3.dispatch=function(){var e=new d,t=-1,n=arguments.length;while(++t0&&(r=e.substring(n+1),e=e.substring(0,n)),arguments.length<2?this[e].on(r):this[e].on(r,t)},d3.format=function(e){var t=ns.exec(e),n=t[1]||" ",r=t[3]||"",i=t[5],s=+t[6],o=t[7],u=t[8],a=t[9],f=1,l="",c=!1;u&&(u=+u.substring(1)),i&&(n="0",o&&(s-=Math.floor((s-1)/4)));switch(a){case"n":o=!0,a="g";break;case"%":f=100,l="%",a="f";break;case"p":f=100,l="%",a="r";break;case"d":c=!0,u=0;break;case"s":f=-1,a="r"}return a=="r"&&!u&&(a="g"),a=rs.get(a)||g,function(e){if(c&&e%1)return"";var t=e<0&&(e=-e)?"-":r;if(f<0){var h=d3.formatPrefix(e,u);e=h.scale(e),l=h.symbol}else e*=f;e=a(e,u);if(i){var p=e.length+t.length;p=^]))?([+\- ])?(#)?(0)?([0-9]+)?(,)?(\.[0-9]+)?([a-zA-Z%])?/,rs=d3.map({g:function(e,t){return e.toPrecision(t)},e:function(e,t){return e.toExponential(t)},f:function(e,t){return e.toFixed(t)},r:function(e,t){return d3.round(e,t=m(e,t)).toFixed(Math.max(0,Math.min(20,t)))}}),is=["y","z","a","f","p","n","μ","m","","k","M","G","T","P","E","Z","Y"].map(b);d3.formatPrefix=function(e,t){var n=0;return e&&(e<0&&(e*=-1),t&&(e=d3.round(e,m(e,t))),n=1+Math.floor(1e-12+Math.log(e)/Math.LN10),n=Math.max(-24,Math.min(24,Math.floor((n<=0?n+1:n-1)/3)*3))),is[8+n/3]};var ss=T(2),os=T(3),us=function(){return x},as=d3.map({linear:us,poly:T,quad:function(){return ss},cubic:function(){return os},sin:function(){return N},exp:function(){return C},circle:function(){return k},elastic:L,back:A,bounce:function(){return O}}),fs=d3.map({"in":x,out:E,"in-out":S,"out-in":function(e){return S(E(e))}});d3.ease=function(e){var t=e.indexOf("-"),n=t>=0?e.substring(0,t):e,r=t>=0?e.substring(t+1):"in";return n=as.get(n)||us,r=fs.get(r)||x,w(r(n.apply(null,Array.prototype.slice.call(arguments,1))))},d3.event=null,d3.transform=function(e){var t=document.createElementNS(d3.ns.prefix.svg,"g");return(d3.transform=function(e){t.setAttribute("transform",e);var n=t.transform.baseVal.consolidate();return new P(n?n.matrix:cs)})(e)},P.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var ls=180/Math.PI,cs={a:1,b:0,c:0,d:1,e:0,f:0};d3.interpolate=function(e,t){var n=d3.interpolators.length,r;while(--n>=0&&!(r=d3.interpolators[n](e,t)));return r},d3.interpolateNumber=function(e,t){return t-=e,function(n){return e+t*n}},d3.interpolateRound=function(e,t){return t-=e,function(n){return Math.round(e+t*n)}},d3.interpolateString=function(e,t){var n,r,i,s=0,o=0,u=[],a=[],f,l;hs.lastIndex=0;for(r=0;n=hs.exec(t);++r)n.index&&u.push(t.substring(s,o=n.index)),a.push({i:u.length,x:n[0]}),u.push(null),s=hs.lastIndex;s180?l+=360:l-f>180&&(f+=360),r.push({i:n.push(n.pop()+"rotate(",null,")")-2,x:d3.interpolateNumber(f,l)})):l&&n.push(n.pop()+"rotate("+l+")"),c!=h?r.push({i:n.push(n.pop()+"skewX(",null,")")-2,x:d3.interpolateNumber(c,h)}):h&&n.push(n.pop()+"skewX("+h+")"),p[0]!=d[0]||p[1]!=d[1]?(i=n.push(n.pop()+"scale(",null,",",null,")"),r.push({i:i-4,x:d3.interpolateNumber(p[0],d[0])},{i:i-2,x:d3.interpolateNumber(p[1],d[1])})):(d[0]!=1||d[1]!=1)&&n.push(n.pop()+"scale("+d+")"),i=r.length,function(e){var t=-1,s;while(++t180?s-=360:s<-180&&(s+=360),function(e){return Y(n+s*e,r+o*e,i+u*e)+""}},d3.interpolateLab=function(e,t){e=d3.lab(e),t=d3.lab(t);var n=e.l,r=e.a,i=e.b,s=t.l-n,o=t.a-r,u=t.b-i;return function(e){return it(n+s*e,r+o*e,i+u*e)+""}},d3.interpolateHcl=function(e,t){e=d3.hcl(e),t=d3.hcl(t);var n=e.h,r=e.c,i=e.l,s=t.h-n,o=t.c-r,u=t.l-i;return s>180?s-=360:s<-180&&(s+=360),function(e){return tt(n+s*e,r+o*e,i+u*e)+""}},d3.interpolateArray=function(e,t){var n=[],r=[],i=e.length,s=t.length,o=Math.min(e.length,t.length),u;for(u=0;u=0;)if(s=n[r])i&&i!==s.nextSibling&&i.parentNode.insertBefore(s,i),i=s;return this},ks.sort=function(e){e=wt.apply(this,arguments);for(var t=-1,n=this.length;++t=Zs?e?"M0,"+s+"A"+s+","+s+" 0 1,1 0,"+ -s+"A"+s+","+s+" 0 1,1 0,"+s+"M0,"+e+"A"+e+","+e+" 0 1,0 0,"+ -e+"A"+e+","+e+" 0 1,0 0,"+e+"Z":"M0,"+s+"A"+s+","+s+" 0 1,1 0,"+ -s+"A"+s+","+s+" 0 1,1 0,"+s+"Z":e?"M"+s*l+","+s*c+"A"+s+","+s+" 0 "+f+",1 "+s*h+","+s*p+"L"+e*h+","+e*p+"A"+e+","+e+" 0 "+f+",0 "+e*l+","+e*c+"Z":"M"+s*l+","+s*c+"A"+s+","+s+" 0 "+f+",1 "+s*h+","+s*p+"L0,0"+"Z"}var t=en,n=tn,r=nn,i=rn;return e.innerRadius=function(n){return arguments.length?(t=u(n),e):t},e.outerRadius=function(t){return arguments.length?(n=u(t),e):n},e.startAngle=function(t){return arguments.length?(r=u(t),e):r},e.endAngle=function(t){return arguments.length?(i=u(t),e):i},e.centroid=function(){var e=(t.apply(this,arguments)+n.apply(this,arguments))/2,s=(r.apply(this,arguments)+i.apply(this,arguments))/2+Ys;return[Math.cos(s)*e,Math.sin(s)*e]},e};var Ys=-Math.PI/2,Zs=2*Math.PI-1e-6;d3.svg.line=function(){return sn(i)};var eo=d3.map({linear:an,"linear-closed":fn,"step-before":ln,"step-after":cn,basis:gn,"basis-open":yn,"basis-closed":bn,bundle:wn,cardinal:dn,"cardinal-open":hn,"cardinal-closed":pn,monotone:Cn});eo.forEach(function(e,t){t.key=e,t.closed=/-closed$/.test(e)});var to=[0,2/3,1/3,0],no=[0,1/3,2/3,0],ro=[0,1/6,2/3,1/6];d3.svg.line.radial=function(){var e=sn(kn);return e.radius=e.x,delete e.x,e.angle=e.y,delete e.y,e},ln.reverse=cn,cn.reverse=ln,d3.svg.area=function(){return Ln(i)},d3.svg.area.radial=function(){var e=Ln(kn);return e.radius=e.x,delete e.x,e.innerRadius=e.x0,delete e.x0,e.outerRadius=e.x1,delete e.x1,e.angle=e.y,delete e.y,e.startAngle=e.y0,delete e.y0,e.endAngle=e.y1,delete e.y1,e},d3.svg.chord=function(){function e(e,u){var a=t(this,s,e,u),f=t(this,o,e,u);return"M"+a.p0+r(a.r,a.p1,a.a1-a.a0)+(n(a,f)?i(a.r,a.p1,a.r,a.p0):i(a.r,a.p1,f.r,f.p0)+r(f.r,f.p1,f.a1-f.a0)+i(f.r,f.p1,a.r,a.p0))+"Z"}function t(e,t,n,r){var i=t.call(e,n,r),s=a.call(e,i,r),o=f.call(e,i,r)+Ys,u=l.call(e,i,r)+Ys;return{r:s,a0:o,a1:u,p0:[s*Math.cos(o),s*Math.sin(o)],p1:[s*Math.cos(u),s*Math.sin(u)]}}function n(e,t){return e.a0==t.a0&&e.a1==t.a1}function r(e,t,n){return"A"+e+","+e+" 0 "+ +(n>Math.PI)+",1 "+t}function i(e,t,n,r){return"Q 0,0 "+r}var s=An,o=On,a=Mn,f=nn,l=rn;return e.radius=function(t){return arguments.length?(a=u(t),e):a},e.source=function(t){return arguments.length?(s=u(t),e):s},e.target=function(t){return arguments.length?(o=u(t),e):o},e.startAngle=function(t){return arguments.length?(f=u(t),e):f},e.endAngle=function(t){return arguments.length?(l=u(t),e):l},e},d3.svg.diagonal=function(){function e(e,i){var s=t.call(this,e,i),o=n.call(this,e,i),u=(s.y+o.y)/2,a=[s,{x:s.x,y:u},{x:o.x,y:u},o];return a=a.map(r),"M"+a[0]+"C"+a[1]+" "+a[2]+" "+a[3]}var t=An,n=On,r=Pn;return e.source=function(n){return arguments.length?(t=u(n),e):t},e.target=function(t){return arguments.length?(n=u(t),e):n},e.projection=function(t){return arguments.length?(r=t,e):r},e},d3.svg.diagonal.radial=function(){var e=d3.svg.diagonal(),t=Pn,n=e.projection;return e.projection=function(e){return arguments.length?n(Hn(t=e)):t},e},d3.svg.mouse=d3.mouse,d3.svg.touches=d3.touches,d3.svg.symbol=function(){function e(e,r){return(io.get(t.call(this,e,r))||Fn)(n.call(this,e,r))}var t=jn,n=Bn;return e.type=function(n){return arguments.length?(t=u(n),e):t},e.size=function(t){return arguments.length?(n=u(t),e):n},e};var io=d3.map({circle:Fn,cross:function(e){var t=Math.sqrt(e/5)/2;return"M"+ -3*t+","+ -t+"H"+ -t+"V"+ -3*t+"H"+t+"V"+ -t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+ -t+"V"+t+"H"+ -3*t+"Z"},diamond:function(e){var t=Math.sqrt(e/(2*oo)),n=t*oo;return"M0,"+ -t+"L"+n+",0"+" 0,"+t+" "+ -n+",0"+"Z"},square:function(e){var t=Math.sqrt(e)/2;return"M"+ -t+","+ -t+"L"+t+","+ -t+" "+t+","+t+" "+ -t+","+t+"Z"},"triangle-down":function(e){var t=Math.sqrt(e/so),n=t*so/2;return"M0,"+n+"L"+t+","+ -n+" "+ -t+","+ -n+"Z"},"triangle-up":function(e){var t=Math.sqrt(e/so),n=t*so/2;return"M0,"+ -n+"L"+t+","+n+" "+ -t+","+n+"Z"}});d3.svg.symbolTypes=io.keys();var so=Math.sqrt(3),oo=Math.tan(30*Math.PI/180);d3.svg.axis=function(){function e(e){e.each(function(){var e=d3.select(this),c=a==null?t.ticks?t.ticks.apply(t,u):t.domain():a,h=f==null?t.tickFormat?t.tickFormat.apply(t,u):String:f,p=Rn(t,c,l),d=e.selectAll(".minor").data(p,String),v=d.enter().insert("line","g").attr("class","tick minor").style("opacity",1e-6),m=d3.transition(d.exit()).style("opacity",1e-6).remove(),g=d3.transition(d).style("opacity",1),y=e.selectAll("g").data(c,String),b=y.enter().insert("g","path").style("opacity",1e-6),w=d3.transition(y.exit()).style("opacity",1e-6).remove(),E=d3.transition(y).style("opacity",1),S,x=Dt(t),T=e.selectAll(".domain").data([0]),N=T.enter().append("path").attr("class","domain"),C=d3.transition(T),k=t.copy(),L=this.__chart__||k;this.__chart__=k,b.append("line").attr("class","tick"),b.append("text");var A=b.select("line"),O=E.select("line"),M=y.select("text").text(h),_=b.select("text"),D=E.select("text");switch(n){case"bottom":S=In,v.attr("y2",i),g.attr("x2",0).attr("y2",i),A.attr("y2",r),_.attr("y",Math.max(r,0)+o),O.attr("x2",0).attr("y2",r),D.attr("x",0).attr("y",Math.max(r,0)+o),M.attr("dy",".71em").attr("text-anchor","middle"),C.attr("d","M"+x[0]+","+s+"V0H"+x[1]+"V"+s);break;case"top":S=In,v.attr("y2",-i),g.attr("x2",0).attr("y2",-i),A.attr("y2",-r),_.attr("y",-(Math.max(r,0)+o)),O.attr("x2",0).attr("y2",-r),D.attr("x",0).attr("y",-(Math.max(r,0)+o)),M.attr("dy","0em").attr("text-anchor","middle"),C.attr("d","M"+x[0]+","+ -s+"V0H"+x[1]+"V"+ -s);break;case"left":S=qn,v.attr("x2",-i),g.attr("x2",-i).attr("y2",0),A.attr("x2",-r),_.attr("x",-(Math.max(r,0)+o)),O.attr("x2",-r).attr("y2",0),D.attr("x",-(Math.max(r,0)+o)).attr("y",0),M.attr("dy",".32em").attr("text-anchor","end"),C.attr("d","M"+ -s+","+x[0]+"H0V"+x[1]+"H"+ -s);break;case"right":S=qn,v.attr("x2",i),g.attr("x2",i).attr("y2",0),A.attr("x2",r),_.attr("x",Math.max(r,0)+o),O.attr("x2",r).attr("y2",0),D.attr("x",Math.max(r,0)+o).attr("y",0),M.attr("dy",".32em").attr("text-anchor","start"),C.attr("d","M"+s+","+x[0]+"H0V"+x[1]+"H"+s)}if(t.ticks)b.call(S,L),E.call(S,k),w.call(S,k),v.call(S,L),g.call(S,k),m.call(S,k);else{var P=k.rangeBand()/2,H=function(e){return k(e)+P};b.call(S,H),E.call(S,H)}})}var t=d3.scale.linear(),n="bottom",r=6,i=6,s=6,o=3,u=[10],a=null,f,l=0;return e.scale=function(n){return arguments.length?(t=n,e):t},e.orient=function(t){return arguments.length?(n=t,e):n},e.ticks=function(){return arguments.length?(u=arguments,e):u},e.tickValues=function(t){return arguments.length?(a=t,e):a},e.tickFormat=function(t){return arguments.length?(f=t,e):f},e.tickSize=function(t,n,o){if(!arguments.length)return r;var u=arguments.length-1;return r=+t,i=u>1?+n:r,s=u>0?+arguments[u]:r,e},e.tickPadding=function(t){return arguments.length?(o=+t,e):o},e.tickSubdivide=function(t){return arguments.length?(l=+t,e):l},e},d3.svg.brush=function(){function e(s){s.each(function(){var s=d3.select(this),f=s.selectAll(".background").data([0]),l=s.selectAll(".extent").data([0]),c=s.selectAll(".resize").data(a,String),h;s.style("pointer-events","all").on("mousedown.brush",i).on("touchstart.brush",i),f.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),l.enter().append("rect").attr("class","extent").style("cursor","move"),c.enter().append("g").attr("class",function(e){return"resize "+e}).style("cursor",function(e){return uo[e]}).append("rect").attr("x",function(e){return/[ew]$/.test(e)?-3:null}).attr("y",function(e){return/^[ns]/.test(e)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),c.style("display",e.empty()?"none":null),c.exit().remove(),o&&(h=Dt(o),f.attr("x",h[0]).attr("width",h[1]-h[0]),n(s)),u&&(h=Dt(u),f.attr("y",h[0]).attr("height",h[1]-h[0]),r(s)),t(s)})}function t(e){e.selectAll(".resize").attr("transform",function(e){return"translate("+f[+/e$/.test(e)][0]+","+f[+/^s/.test(e)][1]+")"})}function n(e){e.select(".extent").attr("x",f[0][0]),e.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1][0]-f[0][0])}function r(e){e.select(".extent").attr("y",f[0][1]),e.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1][1]-f[0][1])}function i(){function i(){var e=d3.event.changedTouches;return e?d3.touches(v,e)[0]:d3.mouse(v)}function a(){d3.event.keyCode==32&&(S||(x=null,T[0]-=f[1][0],T[1]-=f[1][1],S=2),M())}function c(){d3.event.keyCode==32&&S==2&&(T[0]+=f[1][0],T[1]+=f[1][1],S=0,M())}function h(){var e=i(),s=!1;N&&(e[0]+=N[0],e[1]+=N[1]),S||(d3.event.altKey?(x||(x=[(f[0][0]+f[1][0])/2,(f[0][1]+f[1][1])/2]),T[0]=f[+(e[0]0?a=e:a=0:e>0&&(r.start({type:"start",alpha:a=e}),d3.timer(n.tick)),n):a},n.start=function(){function e(e,n){var i=t(r),s=-1,o=i.length,u;while(++si&&(i=u),r.push(u)}for(o=0;o0){s=-1;while(++s=a[0]&&d<=a[1]&&(l=o[d3.bisect(f,d,1,h)-1],l.y+=p,l.push(e[s]))}return o}var t=!0,n=Number,r=fr,i=ur;return e.value=function(t){return arguments.length?(n=t,e):n},e.range=function(t){return arguments.length?(r=u(t),e):r},e.bins=function(t){return arguments.length?(i=typeof t=="number"?function(e){return ar(e,t)}:u(t),e):i},e.frequency=function(n){return arguments.length?(t=!!n,e):t},e},d3.layout.hierarchy=function(){function e(t,o,u){var a=i.call(n,t,o),f=vo?t:{data:t};f.depth=o,u.push(f);if(a&&(c=a.length)){var l=-1,c,h=f.children=[],p=0,d=o+1,v;while(++l0){var l=n*f/2;Hr(o,function(e){e.r+=l}),Hr(o,br),Hr(o,function(e){e.r-=l}),f=Math.max(2*o.r/u,2*o.r/a)}return Sr(o,u/2,a/2,1/f),s}var t=d3.layout.hierarchy().sort(vr),n=0,r=[1,1];return e.size=function(t){return arguments.length?(r=t,e):r},e.padding=function(t){return arguments.length?(n=+t,e):n},lr(e,t)},d3.layout.cluster=function(){function e(e,i){var s=t.call(this,e,i),o=s[0],u,a=0,f,l;Hr(o,function(e){var t=e.children;t&&t.length?(e.x=Nr(t),e.y=Tr(t)):(e.x=u?a+=n(e,u):0,e.y=0,u=e)});var c=Cr(o),h=kr(o),p=c.x-n(c,h)/2,d=h.x+n(h,c)/2;return Hr(o,function(e){e.x=(e.x-p)/(d-p)*r[0],e.y=(1-(o.y?e.y/o.y:1))*r[1]}),s}var t=d3.layout.hierarchy().sort(null).value(null),n=Lr,r=[1,1];return e.separation=function(t){return arguments.length?(n=t,e):n},e.size=function(t){return arguments.length?(r=t,e):r},lr(e,t)},d3.layout.tree=function(){function e(e,i){function s(e,t){var r=e.children,i=e._tree;if(r&&(o=r.length)){var o,a=r[0],f,l=a,c,h=-1;while(++h0&&(jr(Fr(o,e,r),e,h),a+=h,f+=h),l+=o._tree.mod,a+=i._tree.mod,c+=u._tree.mod,f+=s._tree.mod;o&&!Or(s)&&(s._tree.thread=o,s._tree.mod+=l-f),i&&!Ar(u)&&(u._tree.thread=i,u._tree.mod+=a-c,r=e)}return r}var a=t.call(this,e,i),f=a[0];Hr(f,function(e,t){e._tree={ancestor:e,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),s(f),o(f,-f._tree.prelim);var l=Mr(f,Dr),c=Mr(f,_r),h=Mr(f,Pr),p=l.x-n(l,c)/2,d=c.x+n(c,l)/2,v=h.depth||1;return Hr(f,function(e){e.x=(e.x-p)/(d-p)*r[0],e.y=e.depth/v*r[1],delete e._tree}),a}var t=d3.layout.hierarchy().sort(null).value(null),n=Lr,r=[1,1];return e.separation=function(t){return arguments.length?(n=t,e):n},e.size=function(t){return arguments.length?(r=t,e):r},lr(e,t)},d3.layout.treemap=function(){function e(e,t){var n=-1,r=e.length,i,s;while(++n0)u.push(f=a[d-1]),u.area+=f.area,(h=r(u,p))<=c?(a.pop(),c=h):(u.area-=u.pop().area,i(u,p,o,!1),p=Math.min(o.dx,o.dy),u.length=u.area=0,c=Infinity);u.length&&(i(u,p,o,!0),u.length=u.area=0),s.forEach(t)}}function n(t){var r=t.children;if(r&&r.length){var s=l(t),o=r.slice(),u,a=[];e(o,s.dx*s.dy/t.value),a.area=0;while(u=o.pop())a.push(u),a.area+=u.area,u.z!=null&&(i(a,u.z?s.dx:s.dy,s,!o.length),a.length=a.area=0);r.forEach(n)}}function r(e,t){var n=e.area,r,i=0,s=Infinity,o=-1,u=e.length;while(++oi&&(i=r)}return n*=n,t*=t,n?Math.max(t*i*p/n,n/(t*s*p)):Infinity}function i(e,t,n,r){var i=-1,s=e.length,o=n.x,a=n.y,f=t?u(e.area/t):0,l;if(t==n.dx){if(r||f>n.dy)f=n.dy;while(++in.dx)f=n.dx;while(++i50?n:s<-140?r:o<21?i:t)(e)}var t=d3.geo.albers(),n=d3.geo.albers().origin([-160,60]).parallels([55,65]),r=d3.geo.albers().origin([-160,20]).parallels([8,18]),i=d3.geo.albers().origin([-60,10]).parallels([8,18]);return e.scale=function(s){return arguments.length?(t.scale(s),n.scale(s*.6),r.scale(s),i.scale(s*1.5),e.translate(t.translate())):t.scale()},e.translate=function(s){if(!arguments.length)return t.translate();var o=t.scale()/1e3,u=s[0],a=s[1];return t.translate(s),n.translate([u-400*o,a+170*o]),r.translate([u-190*o,a+200*o]),i.translate([u+580*o,a+430*o]),e},e.scale(t.scale())},d3.geo.bonne=function(){function e(e){var u=e[0]*mo-r,a=e[1]*mo-i;if(s){var f=o+s-a,l=u*Math.cos(a)/f;u=f*Math.sin(l),a=f*Math.cos(l)-o}else u*=Math.cos(a),a*=-1;return[t*u+n[0],t*a+n[1]]}var t=200,n=[480,250],r,i,s,o;return e.invert=function(e){var i=(e[0]-n[0])/t,u=(e[1]-n[1])/t;if(s){var a=o+u,f=Math.sqrt(i*i+a*a);u=o+s-f,i=r+f*Math.atan2(i,a)/Math.cos(u)}else u*=-1,i/=Math.cos(u);return[i/mo,u/mo]},e.parallel=function(t){return arguments.length?(o=1/Math.tan(s=t*mo),e):s/mo},e.origin=function(t){return arguments.length?(r=t[0]*mo,i=t[1]*mo,e):[r/mo,i/mo]},e.scale=function( 4 | n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e.origin([0,0]).parallel(45)},d3.geo.equirectangular=function(){function e(e){var r=e[0]/360,i=-e[1]/360;return[t*r+n[0],t*i+n[1]]}var t=500,n=[480,250];return e.invert=function(e){var r=(e[0]-n[0])/t,i=(e[1]-n[1])/t;return[360*r,-360*i]},e.scale=function(n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e},d3.geo.mercator=function(){function e(e){var r=e[0]/360,i=-(Math.log(Math.tan(Math.PI/4+e[1]*mo/2))/mo)/360;return[t*r+n[0],t*Math.max(-0.5,Math.min(.5,i))+n[1]]}var t=500,n=[480,250];return e.invert=function(e){var r=(e[0]-n[0])/t,i=(e[1]-n[1])/t;return[360*r,2*Math.atan(Math.exp(-360*i*mo))/mo-90]},e.scale=function(n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e},d3.geo.path=function(){function e(e,t){typeof s=="function"&&(o=zr(s.apply(this,arguments))),f(e);var n=a.length?a.join(""):null;return a=[],n}function t(e){return u(e).join(",")}function n(e){var t=i(e[0]),n=0,r=e.length;while(++n0){a.push("M");while(++o0){a.push("M");while(++lr&&(r=e),si&&(i=s)}),[[t,n],[r,i]]};var go={Feature:Xr,FeatureCollection:Vr,GeometryCollection:$r,LineString:Jr,MultiLineString:Kr,MultiPoint:Jr,MultiPolygon:Qr,Point:Gr,Polygon:Yr};d3.geo.circle=function(){function e(){}function t(e){return a.distance(e)=l*l+c*c?r[s].index=-1:(r[h].index=-1,d=r[s].angle,h=s,p=o)):(d=r[s].angle,h=s,p=o);i.push(u);for(s=0,o=0;s<2;++o)r[o].index!==-1&&(i.push(r[o].index),s++);v=i.length;for(;o=0?(n=e.ep.r,r=e.ep.l):(n=e.ep.l,r=e.ep.r),e.a===1?(o=n?n.y:-1e6,i=e.c-e.b*o,u=r?r.y:1e6,s=e.c-e.b*u):(i=n?n.x:-1e6,o=e.c-e.a*i,s=r?r.x:1e6,u=e.c-e.a*s);var a=[i,o],f=[s,u];t[e.region.l.index].push(a,f),t[e.region.r.index].push(a,f)}),t.map(function(t,n){var r=e[n][0],i=e[n][1];return t.forEach(function(e){e.angle=Math.atan2(e[0]-r,e[1]-i)}),t.sort(function(e,t){return e.angle-t.angle}).filter(function(e,n){return!n||e.angle-t[n-1].angle>1e-10})})};var wo={l:"r",r:"l"};d3.geom.delaunay=function(e){var t=e.map(function(){return[]}),n=[];return ui(e,function(n){t[n.region.l.index].push(e[n.region.r.index])}),t.forEach(function(t,r){var i=e[r],s=i[0],o=i[1];t.forEach(function(e){e.angle=Math.atan2(e[0]-s,e[1]-o)}),t.sort(function(e,t){return e.angle-t.angle});for(var u=0,a=t.length-1;u=u,l=t.y>=a,c=(l<<1)+f;e.leaf=!1,e=e.nodes[c]||(e.nodes[c]=ai()),f?n=u:i=u,l?r=a:o=a,s(e,t,n,r,i,o)}var u,a=-1,f=e.length;f&&isNaN(e[0].x)&&(e=e.map(li));if(arguments.length<5)if(arguments.length===3)i=r=n,n=t;else{t=n=Infinity,r=i=-Infinity;while(++ar&&(r=u.x),u.y>i&&(i=u.y);var l=r-t,c=i-n;l>c?i=n+l:r=t+c}var h=ai();return h.add=function(e){s(h,e,t,n,r,i)},h.visit=function(e){fi(e,h,t,n,r,i)},e.forEach(h.add),h},d3.time={};var Eo=Date,So=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];ci.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){xo.setUTCDate.apply(this._,arguments)},setDay:function(){xo.setUTCDay.apply(this._,arguments)},setFullYear:function(){xo.setUTCFullYear.apply(this._,arguments)},setHours:function(){xo.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){xo.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){xo.setUTCMinutes.apply(this._,arguments)},setMonth:function(){xo.setUTCMonth.apply(this._,arguments)},setSeconds:function(){xo.setUTCSeconds.apply(this._,arguments)},setTime:function(){xo.setTime.apply(this._,arguments)}};var xo=Date.prototype,To="%a %b %e %H:%M:%S %Y",No="%m/%d/%y",Co="%H:%M:%S",ko=So,Lo=ko.map(hi),Ao=["January","February","March","April","May","June","July","August","September","October","November","December"],Oo=Ao.map(hi);d3.time.format=function(e){function t(t){var r=[],i=-1,s=0,o,u;while(++i=12?"PM":"AM"},S:function(e){return Mo(e.getSeconds())},U:function(e){return Mo(d3.time.sundayOfYear(e))},w:function(e){return e.getDay()},W:function(e){return Mo(d3.time.mondayOfYear(e))},x:d3.time.format(No),X:d3.time.format(Co),y:function(e){return Mo(e.getFullYear()%100)},Y:function(e){return Do(e.getFullYear()%1e4)},Z:Di,"%":function(e){return"%"}},Uo={a:mi,A:gi,b:yi,B:bi,c:wi,d:ki,e:ki,H:Li,I:Li,L:Mi,m:Ci,M:Ai,p:_i,S:Oi,x:Ei,X:Si,y:Ti,Y:xi},zo=/^\s*\d+/,Wo=d3.map({am:0,pm:1});d3.time.format.utc=function(e){function t(e){try{Eo=ci;var t=new Eo;return t._=e,n(t)}finally{Eo=Date}}var n=d3.time.format(e);return t.parse=function(e){try{Eo=ci;var t=n.parse(e);return t&&t._}finally{Eo=Date}},t.toString=n.toString,t};var Xo=d3.time.format.utc("%Y-%m-%dT%H:%M:%S.%LZ");d3.time.format.iso=Date.prototype.toISOString?Pi:Xo,Pi.parse=function(e){var t=new Date(e);return isNaN(t)?null:t},Pi.toString=Xo.toString,d3.time.second=Hi(function(e){return new Eo(Math.floor(e/1e3)*1e3)},function(e,t){e.setTime(e.getTime()+Math.floor(t)*1e3)},function(e){return e.getSeconds()}),d3.time.seconds=d3.time.second.range,d3.time.seconds.utc=d3.time.second.utc.range,d3.time.minute=Hi(function(e){return new Eo(Math.floor(e/6e4)*6e4)},function(e,t){e.setTime(e.getTime()+Math.floor(t)*6e4)},function(e){return e.getMinutes()}),d3.time.minutes=d3.time.minute.range,d3.time.minutes.utc=d3.time.minute.utc.range,d3.time.hour=Hi(function(e){var t=e.getTimezoneOffset()/60;return new Eo((Math.floor(e/36e5-t)+t)*36e5)},function(e,t){e.setTime(e.getTime()+Math.floor(t)*36e5)},function(e){return e.getHours()}),d3.time.hours=d3.time.hour.range,d3.time.hours.utc=d3.time.hour.utc.range,d3.time.day=Hi(function(e){var t=new Eo(1970,0);return t.setFullYear(e.getFullYear(),e.getMonth(),e.getDate()),t},function(e,t){e.setDate(e.getDate()+t)},function(e){return e.getDate()-1}),d3.time.days=d3.time.day.range,d3.time.days.utc=d3.time.day.utc.range,d3.time.dayOfYear=function(e){var t=d3.time.year(e);return Math.floor((e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/864e5)},So.forEach(function(e,t){e=e.toLowerCase(),t=7-t;var n=d3.time[e]=Hi(function(e){return(e=d3.time.day(e)).setDate(e.getDate()-(e.getDay()+t)%7),e},function(e,t){e.setDate(e.getDate()+Math.floor(t)*7)},function(e){var n=d3.time.year(e).getDay();return Math.floor((d3.time.dayOfYear(e)+(n+t)%7)/7)-(n!==t)});d3.time[e+"s"]=n.range,d3.time[e+"s"].utc=n.utc.range,d3.time[e+"OfYear"]=function(e){var n=d3.time.year(e).getDay();return Math.floor((d3.time.dayOfYear(e)+(n+t)%7)/7)}}),d3.time.week=d3.time.sunday,d3.time.weeks=d3.time.sunday.range,d3.time.weeks.utc=d3.time.sunday.utc.range,d3.time.weekOfYear=d3.time.sundayOfYear,d3.time.month=Hi(function(e){return e=d3.time.day(e),e.setDate(1),e},function(e,t){e.setMonth(e.getMonth()+t)},function(e){return e.getMonth()}),d3.time.months=d3.time.month.range,d3.time.months.utc=d3.time.month.utc.range,d3.time.year=Hi(function(e){return e=d3.time.day(e),e.setMonth(0,1),e},function(e,t){e.setFullYear(e.getFullYear()+t)},function(e){return e.getFullYear()}),d3.time.years=d3.time.year.range,d3.time.years.utc=d3.time.year.utc.range;var Vo=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],$o=[[d3.time.second,1],[d3.time.second,5],[d3.time.second,15],[d3.time.second,30],[d3.time.minute,1],[d3.time.minute,5],[d3.time.minute,15],[d3.time.minute,30],[d3.time.hour,1],[d3.time.hour,3],[d3.time.hour,6],[d3.time.hour,12],[d3.time.day,1],[d3.time.day,2],[d3.time.week,1],[d3.time.month,1],[d3.time.month,3],[d3.time.year,1]],Jo=[[d3.time.format("%Y"),function(e){return!0}],[d3.time.format("%B"),function(e){return e.getMonth()}],[d3.time.format("%b %d"),function(e){return e.getDate()!=1}],[d3.time.format("%a %d"),function(e){return e.getDay()&&e.getDate()!=1}],[d3.time.format("%I %p"),function(e){return e.getHours()}],[d3.time.format("%I:%M"),function(e){return e.getMinutes()}],[d3.time.format(":%S"),function(e){return e.getSeconds()}],[d3.time.format(".%L"),function(e){return e.getMilliseconds()}]],Ko=d3.scale.linear(),Qo=qi(Jo);$o.year=function(e,t){return Ko.domain(e.map(Ui)).ticks(t).map(Ri)},d3.time.scale=function(){return ji(d3.scale.linear(),$o,Qo)};var Go=$o.map(function(e){return[e[0].utc,e[1]]}),Yo=[[d3.time.format.utc("%Y"),function(e){return!0}],[d3.time.format.utc("%B"),function(e){return e.getUTCMonth()}],[d3.time.format.utc("%b %d"),function(e){return e.getUTCDate()!=1}],[d3.time.format.utc("%a %d"),function(e){return e.getUTCDay()&&e.getUTCDate()!=1}],[d3.time.format.utc("%I %p"),function(e){return e.getUTCHours()}],[d3.time.format.utc("%I:%M"),function(e){return e.getUTCMinutes()}],[d3.time.format.utc(":%S"),function(e){return e.getUTCSeconds()}],[d3.time.format.utc(".%L"),function(e){return e.getUTCMilliseconds()}]],Zo=qi(Yo);Go.year=function(e,t){return Ko.domain(e.map(Wi)).ticks(t).map(zi)},d3.time.scale.utc=function(){return ji(d3.scale.linear(),Go,Zo)}})(); -------------------------------------------------------------------------------- /assets/stylesheets/custom_report.css: -------------------------------------------------------------------------------- 1 | .custom-report { 2 | width: 100%; 3 | } 4 | 5 | .custom-report-chart { 6 | float: left; 7 | } 8 | 9 | .custom-report-chart svg { 10 | height: 500px; 11 | overflow: visible; 12 | } 13 | -------------------------------------------------------------------------------- /assets/stylesheets/nv.d3.css: -------------------------------------------------------------------------------- 1 | 2 | /******************** 3 | * HTML CSS 4 | */ 5 | 6 | 7 | .chartWrap { 8 | margin: 0; 9 | padding: 0; 10 | overflow: hidden; 11 | } 12 | 13 | 14 | /******************** 15 | * TOOLTIP CSS 16 | */ 17 | 18 | .nvtooltip { 19 | position: absolute; 20 | background-color: rgba(255,255,255,1); 21 | padding: 10px; 22 | border: 1px solid #ddd; 23 | z-index: 10000; 24 | 25 | font-family: Arial; 26 | font-size: 13px; 27 | 28 | transition: opacity 500ms linear; 29 | -moz-transition: opacity 500ms linear; 30 | -webkit-transition: opacity 500ms linear; 31 | 32 | transition-delay: 500ms; 33 | -moz-transition-delay: 500ms; 34 | -webkit-transition-delay: 500ms; 35 | 36 | -moz-box-shadow: 4px 4px 8px rgba(0,0,0,.5); 37 | -webkit-box-shadow: 4px 4px 8px rgba(0,0,0,.5); 38 | box-shadow: 4px 4px 8px rgba(0,0,0,.5); 39 | 40 | -moz-border-radius: 10px; 41 | border-radius: 10px; 42 | 43 | pointer-events: none; 44 | 45 | -webkit-touch-callout: none; 46 | -webkit-user-select: none; 47 | -khtml-user-select: none; 48 | -moz-user-select: none; 49 | -ms-user-select: none; 50 | user-select: none; 51 | } 52 | 53 | .nvtooltip h3 { 54 | margin: 0; 55 | padding: 0; 56 | text-align: center; 57 | } 58 | 59 | .nvtooltip p { 60 | margin: 0; 61 | padding: 0; 62 | text-align: center; 63 | } 64 | 65 | .nvtooltip span { 66 | display: inline-block; 67 | margin: 2px 0; 68 | } 69 | 70 | .nvtooltip-pending-removal { 71 | position: absolute; 72 | pointer-events: none; 73 | } 74 | 75 | 76 | /******************** 77 | * SVG CSS 78 | */ 79 | 80 | 81 | svg { 82 | -webkit-touch-callout: none; 83 | -webkit-user-select: none; 84 | -khtml-user-select: none; 85 | -moz-user-select: none; 86 | -ms-user-select: none; 87 | user-select: none; 88 | /* Trying to get SVG to act like a greedy block in all browsers */ 89 | display: block; 90 | width:100%; 91 | height:100%; 92 | } 93 | 94 | 95 | svg text { 96 | font: normal 12px sans-serif; 97 | } 98 | 99 | svg .title { 100 | font: bold 14px Arial; 101 | } 102 | 103 | .nvd3 .nv-background { 104 | fill: white; 105 | fill-opacity: 0; 106 | /* 107 | pointer-events: none; 108 | */ 109 | } 110 | 111 | .nvd3.nv-noData { 112 | font-size: 18px; 113 | font-weight: bolf; 114 | } 115 | 116 | 117 | /********** 118 | * Brush 119 | */ 120 | 121 | .nv-brush .extent { 122 | fill-opacity: .125; 123 | shape-rendering: crispEdges; 124 | } 125 | 126 | 127 | 128 | /********** 129 | * Legend 130 | */ 131 | 132 | .nvd3 .nv-legend .nv-series { 133 | cursor: pointer; 134 | } 135 | 136 | .nvd3 .nv-legend .disabled circle { 137 | fill-opacity: 0; 138 | } 139 | 140 | 141 | 142 | /********** 143 | * Axes 144 | */ 145 | 146 | .nvd3 .nv-axis path { 147 | fill: none; 148 | stroke: #000; 149 | stroke-opacity: .75; 150 | shape-rendering: crispEdges; 151 | } 152 | 153 | .nvd3 .nv-axis path.domain { 154 | stroke-opacity: .75; 155 | } 156 | 157 | .nvd3 .nv-axis.nv-x path.domain { 158 | stroke-opacity: 0; 159 | } 160 | 161 | .nvd3 .nv-axis line { 162 | fill: none; 163 | stroke: #000; 164 | stroke-opacity: .25; 165 | shape-rendering: crispEdges; 166 | } 167 | 168 | .nvd3 .nv-axis line.zero { 169 | stroke-opacity: .75; 170 | } 171 | 172 | .nvd3 .nv-axis .nv-axisMaxMin text { 173 | font-weight: bold; 174 | } 175 | 176 | .nvd3 .x .nv-axis .nv-axisMaxMin text, 177 | .nvd3 .x2 .nv-axis .nv-axisMaxMin text, 178 | .nvd3 .x3 .nv-axis .nv-axisMaxMin text { 179 | text-anchor: middle 180 | } 181 | 182 | 183 | 184 | /********** 185 | * Brush 186 | */ 187 | 188 | .nv-brush .resize path { 189 | fill: #eee; 190 | stroke: #666; 191 | } 192 | 193 | 194 | 195 | /********** 196 | * Bars 197 | */ 198 | 199 | .nvd3 .nv-bars .negative rect { 200 | zfill: brown; 201 | } 202 | 203 | .nvd3 .nv-bars rect { 204 | zfill: steelblue; 205 | fill-opacity: .75; 206 | 207 | transition: fill-opacity 250ms linear; 208 | -moz-transition: fill-opacity 250ms linear; 209 | -webkit-transition: fill-opacity 250ms linear; 210 | } 211 | 212 | .nvd3 .nv-bars rect:hover { 213 | fill-opacity: 1; 214 | } 215 | 216 | .nvd3 .nv-bars .hover rect { 217 | fill: lightblue; 218 | } 219 | 220 | .nvd3 .nv-bars text { 221 | fill: rgba(0,0,0,0); 222 | } 223 | 224 | .nvd3 .nv-bars .hover text { 225 | fill: rgba(0,0,0,1); 226 | } 227 | 228 | .nvd3 .nv-x.nv-axis text { 229 | transform: rotate(90); 230 | } 231 | 232 | 233 | /********** 234 | * Bars 235 | */ 236 | 237 | .nvd3 .nv-multibar .nv-groups rect, 238 | .nvd3 .nv-multibarHorizontal .nv-groups rect, 239 | .nvd3 .nv-discretebar .nv-groups rect { 240 | stroke-opacity: 0; 241 | 242 | transition: fill-opacity 250ms linear; 243 | -moz-transition: fill-opacity 250ms linear; 244 | -webkit-transition: fill-opacity 250ms linear; 245 | } 246 | 247 | .nvd3 .nv-multibar .nv-groups rect:hover, 248 | .nvd3 .nv-multibarHorizontal .nv-groups rect:hover, 249 | .nvd3 .nv-discretebar .nv-groups rect:hover { 250 | fill-opacity: 1; 251 | } 252 | 253 | .nvd3 .nv-discretebar .nv-groups text, 254 | .nvd3 .nv-multibarHorizontal .nv-groups text { 255 | font-weight: bold; 256 | fill: rgba(0,0,0,1); 257 | stroke: rgba(0,0,0,0); 258 | } 259 | 260 | /*********** 261 | * Pie Chart 262 | */ 263 | 264 | .nvd3.nv-pie path { 265 | stroke-opacity: 0; 266 | 267 | transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; 268 | -moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; 269 | -webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; 270 | 271 | } 272 | 273 | .nvd3.nv-pie .nv-slice text { 274 | stroke: #000; 275 | stroke-width: 0; 276 | } 277 | 278 | .nvd3.nv-pie path { 279 | stroke: #fff; 280 | stroke-width: 1px; 281 | stroke-opacity: 1; 282 | } 283 | 284 | .nvd3.nv-pie .hover path { 285 | fill-opacity: .7; 286 | /* 287 | stroke-width: 6px; 288 | stroke-opacity: 1; 289 | */ 290 | } 291 | 292 | .nvd3.nv-pie .nv-label rect { 293 | fill-opacity: 0; 294 | stroke-opacity: 0; 295 | } 296 | 297 | /********** 298 | * Lines 299 | */ 300 | 301 | .nvd3 .nv-groups path.nv-line { 302 | fill: none; 303 | stroke-width: 2.5px; 304 | stroke-linecap: round; 305 | shape-rendering: geometricPrecision; 306 | 307 | /* 308 | transition: stroke-width 250ms linear; 309 | -moz-transition: stroke-width 250ms linear; 310 | -webkit-transition: stroke-width 250ms linear; 311 | 312 | transition-delay: 250ms 313 | -moz-transition-delay: 250ms; 314 | -webkit-transition-delay: 250ms; 315 | */ 316 | } 317 | 318 | .nvd3 .nv-groups path.nv-area { 319 | stroke: none; 320 | stroke-linecap: round; 321 | shape-rendering: geometricPrecision; 322 | 323 | /* 324 | stroke-width: 2.5px; 325 | transition: stroke-width 250ms linear; 326 | -moz-transition: stroke-width 250ms linear; 327 | -webkit-transition: stroke-width 250ms linear; 328 | 329 | transition-delay: 250ms 330 | -moz-transition-delay: 250ms; 331 | -webkit-transition-delay: 250ms; 332 | */ 333 | } 334 | 335 | .nvd3 .nv-line.hover path { 336 | stroke-width: 6px; 337 | } 338 | 339 | /* 340 | .nvd3.scatter .groups .point { 341 | fill-opacity: 0.1; 342 | stroke-opacity: 0.1; 343 | } 344 | */ 345 | 346 | .nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point { 347 | fill-opacity: 0; 348 | stroke-opacity: 0; 349 | } 350 | 351 | .nvd3.nv-scatter.nv-single-point .nv-groups .nv-point { 352 | fill-opacity: .5 !important; 353 | stroke-opacity: .5 !important; 354 | } 355 | 356 | 357 | .nvd3 .nv-groups .nv-point { 358 | transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 359 | -moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 360 | -webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 361 | } 362 | 363 | .nvd3.nv-scatter .nv-groups .nv-point.hover, 364 | .nvd3 .nv-groups .nv-point.hover { 365 | stroke-width: 20px; 366 | fill-opacity: .5 !important; 367 | stroke-opacity: .5 !important; 368 | } 369 | 370 | 371 | .nvd3 .nv-point-paths path { 372 | stroke: #aaa; 373 | stroke-opacity: 0; 374 | fill: #eee; 375 | fill-opacity: 0; 376 | } 377 | 378 | 379 | 380 | .nvd3 .nv-indexLine { 381 | cursor: ew-resize; 382 | } 383 | 384 | 385 | /********** 386 | * Distribution 387 | */ 388 | 389 | .nvd3 .nv-distribution { 390 | pointer-events: none; 391 | } 392 | 393 | 394 | 395 | /********** 396 | * Scatter 397 | */ 398 | 399 | .nvd3 .nv-groups .nv-point { 400 | pointer-events: none; 401 | } 402 | 403 | .nvd3 .nv-groups .nv-point.hover { 404 | stroke-width: 20px; 405 | stroke-opacity: .5; 406 | } 407 | 408 | .nvd3 .nv-scatter .nv-point.hover { 409 | fill-opacity: 1; 410 | } 411 | 412 | /* 413 | .nv-group.hover .nv-point { 414 | fill-opacity: 1; 415 | } 416 | */ 417 | 418 | 419 | /********** 420 | * Stacked Area 421 | */ 422 | 423 | .nvd3.nv-stackedarea path.nv-area { 424 | fill-opacity: .7; 425 | /* 426 | stroke-opacity: .65; 427 | fill-opacity: 1; 428 | */ 429 | stroke-opacity: 0; 430 | 431 | transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; 432 | -moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; 433 | -webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; 434 | 435 | /* 436 | transition-delay: 500ms; 437 | -moz-transition-delay: 500ms; 438 | -webkit-transition-delay: 500ms; 439 | */ 440 | 441 | } 442 | 443 | .nvd3.nv-stackedarea path.nv-area.hover { 444 | fill-opacity: .9; 445 | /* 446 | stroke-opacity: .85; 447 | */ 448 | } 449 | /* 450 | .d3stackedarea .groups path { 451 | stroke-opacity: 0; 452 | } 453 | */ 454 | 455 | 456 | 457 | .nvd3.nv-stackedarea .nv-groups .nv-point { 458 | stroke-opacity: 0; 459 | fill-opacity: 0; 460 | } 461 | 462 | .nvd3.nv-stackedarea .nv-groups .nv-point.hover { 463 | stroke-width: 20px; 464 | stroke-opacity: .75; 465 | fill-opacity: 1; 466 | } 467 | 468 | 469 | 470 | /********** 471 | * Line Plus Bar 472 | */ 473 | 474 | .nvd3.nv-linePlusBar .nv-bar rect { 475 | fill-opacity: .75; 476 | } 477 | 478 | .nvd3.nv-linePlusBar .nv-bar rect:hover { 479 | fill-opacity: 1; 480 | } 481 | 482 | 483 | /********** 484 | * Bullet 485 | */ 486 | 487 | .nvd3.nv-bullet { font: 10px sans-serif; } 488 | .nvd3.nv-bullet rect { fill-opacity: .6; } 489 | .nvd3.nv-bullet rect:hover { fill-opacity: 1; } 490 | .nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; } 491 | .nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; } 492 | .nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; } 493 | .nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; } 494 | .nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; } 495 | .nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; } 496 | .nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; } 497 | .nvd3.nv-bullet .nv-subtitle { fill: #999; } 498 | 499 | 500 | 501 | 502 | /********** 503 | * Sparkline 504 | */ 505 | 506 | .nvd3.nv-sparkline path { 507 | fill: none; 508 | } 509 | 510 | .nvd3.nv-sparklineplus g.nv-hoverValue { 511 | pointer-events: none; 512 | } 513 | 514 | .nvd3.nv-sparklineplus .nv-hoverValue line { 515 | stroke: #f44; 516 | stroke-width: 1.5px; 517 | } 518 | 519 | .nvd3.nv-sparklineplus, 520 | .nvd3.nv-sparklineplus g { 521 | pointer-events: all; 522 | } 523 | 524 | .nvd3 .nv-hoverArea { 525 | fill-opacity: 0; 526 | stroke-opacity: 0; 527 | } 528 | 529 | .nvd3.nv-sparklineplus .nv-xValue, 530 | .nvd3.nv-sparklineplus .nv-yValue { 531 | /* 532 | stroke: #666; 533 | */ 534 | stroke-width: 0; 535 | font-size: .9em; 536 | font-weight: normal; 537 | } 538 | 539 | .nvd3.nv-sparklineplus .nv-yValue { 540 | stroke: #f66; 541 | } 542 | 543 | .nvd3.nv-sparklineplus .nv-maxValue { 544 | stroke: #2ca02c; 545 | fill: #2ca02c; 546 | } 547 | 548 | .nvd3.nv-sparklineplus .nv-minValue { 549 | stroke: #d62728; 550 | fill: #d62728; 551 | } 552 | 553 | .nvd3.nv-sparklineplus .nv-currentValue { 554 | /* 555 | stroke: #444; 556 | fill: #000; 557 | */ 558 | font-weight: bold; 559 | font-size: 1.1em; 560 | } 561 | 562 | /********** 563 | * historical stock 564 | */ 565 | 566 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick { 567 | stroke-width: 2px; 568 | } 569 | 570 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover { 571 | stroke-width: 4px; 572 | } 573 | 574 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive { 575 | stroke: #2ca02c; 576 | } 577 | 578 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative { 579 | stroke: #d62728; 580 | } 581 | 582 | .nvd3.nv-historicalStockChart .nv-axis .nv-axislabel { 583 | font-weight: bold; 584 | } 585 | 586 | .nvd3.nv-historicalStockChart .nv-dragTarget { 587 | fill-opacity: 0; 588 | stroke: none; 589 | cursor: move; 590 | } 591 | 592 | .nvd3 .nv-brush .extent { 593 | /* 594 | cursor: ew-resize !important; 595 | */ 596 | fill-opacity: 0 !important; 597 | } 598 | 599 | .nvd3 .nv-brushBackground rect { 600 | stroke: #000; 601 | stroke-width: .4; 602 | fill: #fff; 603 | fill-opacity: .7; 604 | } 605 | 606 | 607 | 608 | /********** 609 | * Indented Tree 610 | */ 611 | 612 | 613 | /** 614 | * TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library 615 | */ 616 | .nvd3.nv-indentedtree .name { 617 | margin-left: 5px; 618 | } 619 | 620 | .nvd3.nv-indentedtree .clickable { 621 | color: #08C; 622 | cursor: pointer; 623 | } 624 | 625 | .nvd3.nv-indentedtree span.clickable:hover { 626 | color: #005580; 627 | text-decoration: underline; 628 | } 629 | 630 | 631 | .nvd3.nv-indentedtree .nv-childrenCount { 632 | display: inline-block; 633 | margin-left: 5px; 634 | } 635 | 636 | .nvd3.nv-indentedtree .nv-treeicon { 637 | cursor: pointer; 638 | /* 639 | cursor: n-resize; 640 | */ 641 | } 642 | 643 | .nvd3.nv-indentedtree .nv-treeicon.nv-folded { 644 | cursor: pointer; 645 | /* 646 | cursor: s-resize; 647 | */ 648 | } 649 | 650 | /********** 651 | * undevPieChart 652 | */ 653 | 654 | .nv-hint { 655 | opacity: 0; 656 | } 657 | 658 | .nv-hint.always-visible { 659 | opacity: 0.3; 660 | } 661 | 662 | .nv-hint.always-visible .nv-hint-value { 663 | opacity: 0; 664 | } 665 | 666 | .nv-slice:hover .nv-hint { 667 | opacity: 1; 668 | } 669 | 670 | .nv-slice:hover .nv-hint.always-visible .nv-hint-value { 671 | opacity: 1; 672 | } 673 | 674 | .nv-label rect { 675 | fill: white; 676 | } 677 | -------------------------------------------------------------------------------- /config/database-mysql-travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/database-setup/#MySQL 2 | test: 3 | adapter: mysql2 4 | database: redmine 5 | username: travis 6 | encoding: utf8 7 | -------------------------------------------------------------------------------- /config/database-postgresql-travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/database-setup/#PostgreSQL 2 | test: 3 | adapter: postgresql 4 | database: redmine 5 | username: postgres 6 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | label_custom_report_plural: "Custom reports" 3 | label_custom_report_view_all: "View all custom reports" 4 | label_custom_report_new: "Create new custom report" 5 | label_custom_report_public_list: "Public custom reports" 6 | label_custom_report_own_list: "My custom reports" 7 | label_custom_report_edit: "Edit custom report" 8 | label_series_filters: "Series filters" 9 | 10 | label_chart_type_undev_pie: "Undev Pie" 11 | label_chart_type_pie: "Pie" 12 | label_chart_type_donut: "Donut" 13 | label_chart_type_bar: "Discrete Bar" 14 | label_chart_type_horizontal_bar: "Horizontal Bar" 15 | label_chart_type_stacked_bar: "Stacked Bar" 16 | 17 | field_chart_type: "Chart type" 18 | field_null_text: "Text for null values" 19 | field_series_name: "Name of series" 20 | 21 | message_custom_reports_created: "Custom report created successfully" 22 | message_custom_reports_updated: "Custom report updated successfully" 23 | message_custom_reports_destroyed: "Custom report destroyed successfully" 24 | message_cant_delete_last_series: "Custom report must have at least one series!" 25 | message_add_custom_report_series: "Add new series" 26 | message_remove_custom_report_series: "Remove new series" 27 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | label_custom_report_plural: "Reportes" 3 | label_custom_report_view_all: "Ver todos los reportes" 4 | label_custom_report_new: "Crear nuevo reporte" 5 | label_custom_report_public_list: "Reportes públicos" 6 | label_custom_report_own_list: "Mis reportes" 7 | label_custom_report_edit: "Editar reporte" 8 | label_series_filters: "Serie de filtros" 9 | 10 | label_chart_type_undev_pie: "Undev Pastel" 11 | label_chart_type_pie: "Pastel" 12 | label_chart_type_donut: "Dona" 13 | label_chart_type_bar: "Barras" 14 | label_chart_type_horizontal_bar: "Barras horizontales" 15 | label_chart_type_stacked_bar: "Barras apiladas" 16 | 17 | field_chart_type: "Tipo de gráfico" 18 | field_null_text: "Texto para valores nulos" 19 | field_series_name: "Nombre de serie" 20 | 21 | message_custom_reports_created: "Reporte creado exitósamente" 22 | message_custom_reports_updated: "Reporte actualizado exitósamente" 23 | message_custom_reports_destroyed: "Reporte destruído exitósamente" 24 | message_cant_delete_last_series: "Reporte debe tener al menos una serie !" 25 | message_add_custom_report_series: "Agregar nueva serie" 26 | message_remove_custom_report_series: "Remover nueva serie" 27 | -------------------------------------------------------------------------------- /config/locales/hu.yml: -------------------------------------------------------------------------------- 1 | hu: 2 | label_custom_report_plural: "Egyedi riportok" 3 | label_custom_report_view_all: "Minden egyedi riport" 4 | label_custom_report_new: "Egyedi riport létrehozása" 5 | label_custom_report_public_list: "Nyilvános egyedi riportok" 6 | label_custom_report_own_list: "Saját egyedi riportok" 7 | label_custom_report_edit: "Egyedi riport szerkesztése" 8 | label_series_filters: "Adatsor szűrés" 9 | 10 | label_chart_type_undev_pie: "Undev Pie" 11 | label_chart_type_pie: "Pie" 12 | label_chart_type_donut: "Donut" 13 | label_chart_type_bar: "Discrete Bar" 14 | label_chart_type_horizontal_bar: "Horizontal Bar" 15 | label_chart_type_stacked_bar: "Stacked Bar" 16 | 17 | field_chart_type: "Diagram típusa" 18 | field_null_text: "Hiányzó értékek címkéje" 19 | field_series_name: "Adatsor neve" 20 | 21 | message_custom_reports_created: "Az egyedi riport létrehozása sikeresen megtörtént" 22 | message_custom_reports_updated: "Az egyedi riport frissítése sikeresen megtörtént" 23 | message_custom_reports_destroyed: "Az egyedi riport törlése sikeresen megtörtént" 24 | message_cant_delete_last_series: "Legalább egy adatsor meg kell adni!" 25 | message_add_custom_report_series: "Új adatsor hozzáadása" 26 | message_remove_custom_report_series: "Adatsor eltávolítása" 27 | -------------------------------------------------------------------------------- /config/locales/pt-br.yml: -------------------------------------------------------------------------------- 1 | pt-br: 2 | label_custom_report_plural: "Relatórios personalizados" 3 | label_custom_report_view_all: "Visualizar todos os relatórios personalizados" 4 | label_custom_report_new: "Criar um novo relatório personalizado" 5 | label_custom_report_public_list: "Relatórios públicos personalizados" 6 | label_custom_report_own_list: "Meus relatórios personalizados" 7 | label_custom_report_edit: "Editar um relatório personalizado" 8 | label_series_filters: "Séries e filtros" 9 | 10 | label_chart_type_undev_pie: "Rosca com valores" 11 | label_chart_type_pie: "Pizza" 12 | label_chart_type_donut: "Rosca" 13 | label_chart_type_bar: "Barras Vertical" 14 | label_chart_type_horizontal_bar: "Barras Horizontal" 15 | label_chart_type_stacked_bar: "Barras agrupados" 16 | 17 | field_chart_type: "Tipo de gráfico" 18 | field_null_text: "Texto para valores nulos" 19 | field_series_name: "Nomda da séria" 20 | 21 | message_custom_reports_created: "Relatório personalizado criado com sucesso" 22 | message_custom_reports_updated: "Relatório personalizado atualizado com sucesso" 23 | message_custom_reports_destroyed: "Relatório personalizado removido com sucesso" 24 | message_cant_delete_last_series: "Relatório personalizado deve ter pelo menos uma série!" 25 | message_add_custom_report_series: "Adicionar uma nova séria" 26 | message_remove_custom_report_series: "Remover séria nova" 27 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | label_custom_report_plural: "Отчеты" 3 | label_custom_report_view_all: "Список всех отчетов" 4 | label_custom_report_new: "Добавить новый отчет" 5 | label_custom_report_public_list: "Общие отчеты" 6 | label_custom_report_own_list: "Мои отчеты" 7 | label_custom_report_edit: "Изменение отчета" 8 | label_series_filters: "Фильтры рядов данных" 9 | 10 | label_chart_type_undev_pie: "Undev Кольцевая диаграмма" 11 | label_chart_type_pie: "Круговая диаграмма" 12 | label_chart_type_donut: "Кольцевая диаграмма" 13 | label_chart_type_bar: "Столбчатая диаграмма" 14 | label_chart_type_horizontal_bar: "Гистограмма" 15 | label_chart_type_stacked_bar: "Группированная столбчатая диаграмма" 16 | 17 | field_chart_type: "Тип диаграммы" 18 | field_null_text: "Текст для пустых значений" 19 | field_series_name: "Название ряда" 20 | 21 | message_custom_reports_created: "Отчет успешно изменен" 22 | message_custom_reports_updated: "Отчет успешно обновлен" 23 | message_custom_reports_destroyed: "Отчет успешно удален" 24 | message_cant_delete_last_series: "В отчете должен быть по крайней мере один ряд данных!" 25 | message_add_custom_report_series: "Добавить ряд" 26 | message_remove_custom_report_series: "Удалить ряд" 27 | -------------------------------------------------------------------------------- /config/locales/tr.yml: -------------------------------------------------------------------------------- 1 | # Adnan Topcu 2 | tr: 3 | label_custom_report_plural: "Özel Raporlar" 4 | label_custom_report_view_all: "Tüm özel raporları görüntüleme" 5 | label_custom_report_new: "Yeni özel rapor oluştur" 6 | label_custom_report_public_list: "Ortak özel raporlar" 7 | label_custom_report_own_list: "Kendi özel raporlarım" 8 | label_custom_report_edit: "Özel rapor düzenleme" 9 | label_series_filters: "Seri filtreleri" 10 | 11 | label_chart_type_undev_pie: "Undev Pie" 12 | label_chart_type_pie: "Pie" 13 | label_chart_type_donut: "Donut" 14 | label_chart_type_bar: "Discrete Bar" 15 | label_chart_type_horizontal_bar: "Horizontal Bar" 16 | label_chart_type_stacked_bar: "Stacked Bar" 17 | 18 | field_chart_type: "Grafik tipi" 19 | field_null_text: "Boş değerleri için açıklama" 20 | field_series_name: "Seri adı" 21 | 22 | message_custom_reports_created: "Özel rapor başarıyla oluşturuldu" 23 | message_custom_reports_updated: "Özel rapor başarıyla güncellendi" 24 | message_custom_reports_destroyed: "Özel rapor başarıyla silindi" 25 | message_cant_delete_last_series: "Özel rapor enaz bir seriye sahip olmalıdır!" 26 | message_add_custom_report_series: "Yeni seri ekle" 27 | message_remove_custom_report_series: "Seri kaldır" 28 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | RedmineApp::Application.routes.draw do 2 | resources :projects do 3 | resources :custom_reports 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20121212125001_create_custom_reports.rb: -------------------------------------------------------------------------------- 1 | class CreateCustomReports < ActiveRecord::Migration 2 | def self.up 3 | unless table_exists? :custom_reports 4 | create_table :custom_reports do |t| 5 | t.column :project_id, :integer 6 | t.column :user_id, :integer, default: 0, null: false 7 | t.column :is_public, :boolean, default: false, null: false 8 | t.column :name, :string, default: '', null: false 9 | t.column :description, :text 10 | t.column :filters, :text 11 | t.column :group_by, :string, default: '', null: false 12 | t.column :chart_type, :string 13 | t.column :null_text, :string, default: 'Null', null: false 14 | 15 | t.timestamps 16 | end 17 | add_index :custom_reports, :project_id 18 | add_index :custom_reports, :user_id 19 | end 20 | end 21 | 22 | def self.down 23 | if table_exists? :custom_reports 24 | remove_index :custom_reports, :project_id 25 | remove_index :custom_reports, :user_id 26 | drop_table :custom_reports 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /db/migrate/20121212125002_create_custom_report_series.rb: -------------------------------------------------------------------------------- 1 | class CreateCustomReportSeries < ActiveRecord::Migration 2 | def self.up 3 | unless table_exists? :custom_report_series 4 | create_table :custom_report_series do |t| 5 | t.column :custom_report_id, :integer 6 | t.column :name, :string 7 | t.column :filters, :text 8 | 9 | t.timestamps 10 | end 11 | add_index :custom_report_series, :custom_report_id 12 | end 13 | end 14 | 15 | def self.down 16 | if table_exists? :custom_report_series 17 | remove_index :custom_report_series, :custom_report_id 18 | drop_table :custom_report_series 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20121212125003_remove_filters_from_custom_reports.rb: -------------------------------------------------------------------------------- 1 | class RemoveFiltersFromCustomReports < ActiveRecord::Migration 2 | def self.up 3 | if column_exists? :custom_reports, :filters 4 | remove_column :custom_reports, :filters 5 | end 6 | end 7 | 8 | def self.down 9 | unless column_exists? :custom_reports, :filters 10 | add_column :custom_reports, :filters, :string 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | Redmine::Plugin.register :redmine_custom_reports do 2 | name 'Redmine Custom Reports (with charts) plugin' 3 | author 'Restream' 4 | description 'Redmine plugin for custom reports with charts' 5 | version '0.1.5' 6 | url 'https://github.com/Restream/redmine_custom_reports' 7 | author_url 'https://github.com/Restream' 8 | 9 | project_module :custom_reports do 10 | permission :manage_custom_reports, 11 | { custom_reports: [:new, :create, :edit, :update, :destroy] } 12 | permission :view_custom_reports, { custom_reports: [:index, :show] } 13 | permission :manage_public_custom_reports, {} 14 | end 15 | 16 | menu :project_menu, 17 | :custom_reports, 18 | { controller: 'custom_reports', action: 'index' }, 19 | param: :project_id, 20 | before: :settings 21 | end 22 | 23 | # Require plugin after register 24 | require 'redmine_custom_reports' 25 | -------------------------------------------------------------------------------- /lib/redmine_custom_reports.rb: -------------------------------------------------------------------------------- 1 | ActionDispatch::Callbacks.to_prepare do 2 | 3 | # Requiring plugin's controller and model 4 | require_dependency 'custom_report' 5 | require_dependency 'custom_report_series' 6 | require_dependency 'query_ext' 7 | require_dependency 'custom_reports_helper' 8 | require_dependency 'custom_reports_controller' 9 | 10 | # Check that patches applied on every request 11 | load 'redmine_custom_reports/project_patch.rb' 12 | load 'redmine_custom_reports/user_patch.rb' 13 | 14 | end -------------------------------------------------------------------------------- /lib/redmine_custom_reports/project_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'project' 2 | 3 | module RedmineCustomReports 4 | module ProjectPatch 5 | def self.included(base) 6 | base.send :has_many, :custom_reports, dependent: :destroy 7 | end 8 | end 9 | end 10 | 11 | unless Project.included_modules.include? RedmineCustomReports::ProjectPatch 12 | Project.send :include, RedmineCustomReports::ProjectPatch 13 | end -------------------------------------------------------------------------------- /lib/redmine_custom_reports/user_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'user' 2 | 3 | module RedmineCustomReports 4 | module UserPatch 5 | def self.included(base) 6 | base.send :has_many, :custom_reports, dependent: :destroy 7 | end 8 | end 9 | end 10 | 11 | unless User.included_modules.include? RedmineCustomReports::UserPatch 12 | User.send :include, RedmineCustomReports::UserPatch 13 | end -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/redmine_custom_reports/72ca6785ff3d73689a5944b52b4603209e5bfd13/screenshot.png -------------------------------------------------------------------------------- /test/functional/custom_reports_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | 3 | class CustomReportsControllerTest < ActionController::TestCase 4 | fixtures :projects, :trackers, :issue_statuses, :issues, 5 | :enumerations, :users, :issue_categories, 6 | :projects_trackers, 7 | :roles, 8 | :member_roles, 9 | :members 10 | 11 | def setup 12 | @project = Project.find(1) 13 | @project.enable_module! :custom_reports 14 | 15 | # admin 16 | @user = User.find(1) 17 | User.current = @user 18 | 19 | @request = ActionController::TestRequest.new 20 | @request.session[:user_id] = @user.id 21 | 22 | @custom_report = @project.custom_reports.create!( 23 | name: 'name', 24 | description: 'description', 25 | group_by: 'status', 26 | user_id: @user.id, 27 | is_public: true, 28 | chart_type: 'pie', 29 | series_attributes: [{ 30 | name: 'series1', 31 | filters: {} 32 | }] 33 | ) 34 | end 35 | 36 | def test_show_all_custom_reports 37 | get :index, project_id: @project.identifier 38 | assert_response :success 39 | end 40 | 41 | def test_show_custom_report 42 | get :new, project_id: @project.identifier, id: @custom_report.id 43 | assert_response :success 44 | end 45 | 46 | def test_show_new_custom_report 47 | get :new, project_id: @project.identifier 48 | assert_response :success 49 | end 50 | 51 | def test_create_custom_report 52 | series_name = 'series2-1' 53 | series_filters = { 'status_id' => { operator: '=', values: ['1'] } } 54 | attrs = { 55 | name: 'name2', 56 | description: 'description2', 57 | group_by: 'status', 58 | user_id: @user.id, 59 | is_public: true, 60 | chart_type: 'donut', 61 | series_attributes: { 62 | 0 => { 63 | name: series_name, 64 | flt: { 65 | f: ['status_id'], 66 | op: { status_id: '=' }, 67 | v: { status_id: ['1'] } 68 | } 69 | } 70 | } 71 | } 72 | post :create, project_id: @project.identifier, custom_report: attrs 73 | assert_response :redirect 74 | custom_report = @project.custom_reports.find_by_name(attrs[:name]) 75 | assert custom_report 76 | assert_equal attrs[:description], custom_report.description 77 | assert_equal attrs[:group_by], custom_report.group_by 78 | assert_equal attrs[:user_id], custom_report.user_id 79 | assert_equal attrs[:is_public], custom_report.is_public 80 | assert_equal attrs[:chart_type], custom_report.chart_type 81 | 82 | assert_equal 1, custom_report.series.count 83 | series = custom_report.series.first 84 | assert_equal series_name, series.name 85 | assert_equal series_filters, series.filters 86 | end 87 | 88 | def test_show_edit_custom_report 89 | get :edit, project_id: @project.identifier, id: @custom_report.id 90 | assert_response :success 91 | end 92 | 93 | def test_update_custom_report 94 | old_series = @custom_report.series.first 95 | series_name = 'series2-1' 96 | series_filters = { 'status_id' => { operator: '=', values: ['1'] } } 97 | attrs = { 98 | name: 'name2', 99 | description: 'description2', 100 | group_by: 'status', 101 | user_id: @user.id, 102 | is_public: true, 103 | chart_type: 'donut', 104 | series_attributes: { 105 | 0 => { 106 | id: old_series.id, 107 | _destroy: true 108 | }, 109 | 1 => { 110 | name: series_name, 111 | flt: { 112 | f: ['status_id'], 113 | op: { status_id: '=' }, 114 | v: { status_id: ['1'] } 115 | } 116 | } 117 | } 118 | } 119 | put :update, project_id: @project.identifier, id: @custom_report.id, custom_report: attrs 120 | assert_response :redirect 121 | @custom_report.reload 122 | assert_equal attrs[:description], @custom_report.description 123 | assert_equal attrs[:group_by], @custom_report.group_by 124 | assert_equal attrs[:user_id], @custom_report.user_id 125 | assert_equal attrs[:is_public], @custom_report.is_public 126 | assert_equal attrs[:chart_type], @custom_report.chart_type 127 | 128 | assert_equal 1, @custom_report.series.count 129 | series = @custom_report.series.first 130 | assert_equal series_name, series.name 131 | assert_equal series_filters, series.filters 132 | end 133 | 134 | def test_destroy_custom_report 135 | delete :destroy, project_id: @project.identifier, id: @custom_report.id 136 | assert_response :redirect 137 | custom_report = CustomReport.find_by_id @custom_report.id 138 | assert_nil custom_report 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the normal Rails helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | --------------------------------------------------------------------------------