decidim-survey_results 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +30 -0
  4. data/Rakefile +9 -0
  5. data/app/controllers/decidim/survey_results/admin/application_controller.rb +15 -0
  6. data/app/controllers/decidim/survey_results/application_controller.rb +13 -0
  7. data/app/controllers/decidim/survey_results/survey_results_controller.rb +32 -0
  8. data/app/helpers/decidim/survey_results/application_helper.rb +10 -0
  9. data/app/helpers/decidim/survey_results/survey_results_helper.rb +30 -0
  10. data/app/models/decidim/survey_results/application_record.rb +10 -0
  11. data/app/models/decidim/survey_results/files_question_results.rb +60 -0
  12. data/app/models/decidim/survey_results/full_questionnaire.rb +54 -0
  13. data/app/models/decidim/survey_results/matrix_question_results.rb +61 -0
  14. data/app/models/decidim/survey_results/options_question_results.rb +35 -0
  15. data/app/models/decidim/survey_results/results.rb +68 -0
  16. data/app/models/decidim/survey_results/separator_question_results.rb +16 -0
  17. data/app/models/decidim/survey_results/sorting_question_results.rb +71 -0
  18. data/app/models/decidim/survey_results/text_question_results.rb +61 -0
  19. data/app/overrides/decidim/forms/questionnaires/questionnaire_show.rb +9 -0
  20. data/app/packs/entrypoints/decidim_survey_results.js +6 -0
  21. data/app/packs/entrypoints/decidim_survey_results.scss +1 -0
  22. data/app/packs/images/decidim/survey_results/condition_question.png +0 -0
  23. data/app/packs/images/decidim/survey_results/icon.svg +1 -0
  24. data/app/packs/src/decidim/survey_results/application.js +2 -0
  25. data/app/packs/src/decidim/survey_results/chart_helper.js +24 -0
  26. data/app/packs/src/decidim/survey_results/charts.js +108 -0
  27. data/app/packs/stylesheets/decidim/survey_results/survey_results.scss +127 -0
  28. data/app/views/decidim/survey_results/survey_results/_answered_table.html.erb +21 -0
  29. data/app/views/decidim/survey_results/survey_results/_files_question_results.html.erb +9 -0
  30. data/app/views/decidim/survey_results/survey_results/_matrix_question_results.html.erb +26 -0
  31. data/app/views/decidim/survey_results/survey_results/_options_question_results.html.erb +24 -0
  32. data/app/views/decidim/survey_results/survey_results/_participants_icon.html.erb +15 -0
  33. data/app/views/decidim/survey_results/survey_results/_questions.html.erb +25 -0
  34. data/app/views/decidim/survey_results/survey_results/_separator_question_results.html.erb +1 -0
  35. data/app/views/decidim/survey_results/survey_results/_sorting_question_results.html.erb +26 -0
  36. data/app/views/decidim/survey_results/survey_results/_text_question_results.html.erb +9 -0
  37. data/app/views/decidim/survey_results/survey_results/_title_and_description.html.erb +3 -0
  38. data/app/views/decidim/survey_results/survey_results/show.html.erb +17 -0
  39. data/app/views/layouts/decidim/_head.html.erb +36 -0
  40. data/config/assets.rb +9 -0
  41. data/config/i18n-tasks.yml +144 -0
  42. data/config/locales/ca.yml +37 -0
  43. data/config/locales/en.yml +37 -0
  44. data/config/locales/es.yml +37 -0
  45. data/lib/decidim/survey_results/admin_engine.rb +26 -0
  46. data/lib/decidim/survey_results/engine.rb +32 -0
  47. data/lib/decidim/survey_results/extend_components.rb +6 -0
  48. data/lib/decidim/survey_results/test/factories.rb +13 -0
  49. data/lib/decidim/survey_results/version.rb +13 -0
  50. data/lib/decidim/survey_results.rb +11 -0
  51. data/lib/decidim-survey_results.rb +3 -0
  52. metadata +179 -0
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Decidim::SurveyResults
2
+
3
+ This module enables the admins to show survey results to participants through a new survey results page.
4
+ WARNING: This module is in Beta, use it at your own risk.
5
+
6
+ ## Usage
7
+
8
+ SurveyResults will be available as a Component for a Participatory Space.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem "decidim-survey_results"
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ ```bash
21
+ bundle
22
+ ```
23
+
24
+ ## Contributing
25
+
26
+ See [Decidim](https://github.com/decidim/decidim).
27
+
28
+ ## License
29
+
30
+ This engine is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/dev/common_rake"
4
+
5
+ desc "Generates a dummy app for testing"
6
+ task test_app: "decidim:generate_external_test_app"
7
+
8
+ desc "Generates a development app."
9
+ task development_app: "decidim:generate_external_development_app"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ module Admin
6
+ # This controller is the abstract class from which all other controllers of
7
+ # this engine inherit.
8
+ #
9
+ # Note that it inherits from `Decidim::Admin::Components::BaseController`, which
10
+ # override its layout and provide all kinds of useful methods.
11
+ class ApplicationController < Decidim::Admin::Components::BaseController
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ # This controller is the abstract class from which all other controllers of
6
+ # this engine inherit.
7
+ #
8
+ # Note that it inherits from `Decidim::Components::BaseController`, which
9
+ # override its layout and provide all kinds of useful methods.
10
+ class ApplicationController < Decidim::Components::BaseController
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class SurveyResultsController < ApplicationController
6
+ include Decidim::ApplicationHelper
7
+ include Decidim::SurveyResults::SurveyResultsHelper
8
+
9
+ def show
10
+ @full_questionnaire = FullQuestionnaire.new(questionnaire)
11
+ end
12
+
13
+ def current_component
14
+ @current_component ||= Decidim::Component.find(params[:component_id])
15
+ end
16
+
17
+ def current_participatory_space
18
+ @current_participatory_space ||= current_component.participatory_space
19
+ end
20
+
21
+ private
22
+
23
+ def survey
24
+ @survey ||= ::Decidim::Surveys::Survey.find_by(component: current_component)
25
+ end
26
+
27
+ def questionnaire
28
+ @questionnaire ||= ::Decidim::Forms::Questionnaire.find_by(questionnaire_for: survey)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ # Custom helpers, scoped to the survey_results engine.
6
+ #
7
+ module ApplicationHelper
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ module SurveyResultsHelper
6
+
7
+ def render_question_results(full_questionnaire, question)
8
+ if question.question_type == "title_and_description"
9
+ render partial: question.question_type, locals: { question: question }
10
+ else
11
+ results= Results.for(full_questionnaire, question)
12
+ render partial: results.partial_name, locals: {
13
+ full_questionnaire:,
14
+ results:,
15
+ labels: results.x_labels,
16
+ datasets: results.datasets
17
+ }
18
+ end
19
+ end
20
+
21
+ def conditioned_question?(question)
22
+ Decidim::Forms::DisplayCondition.where(decidim_question_id: question).present?
23
+ end
24
+
25
+ def title_and_description_question?(question)
26
+ question.question_type == "title_and_description"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ # Abstract class from which all models in this engine inherit.
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class FilesQuestionResults < Results
6
+
7
+ def initialize(full_questionnaire, question)
8
+ super(full_questionnaire, question, "files_question_results")
9
+ end
10
+
11
+ def compute_results
12
+ data= {
13
+ labels: [
14
+ I18n.t("survey_results.question_results.answered"),
15
+ I18n.t("survey_results.question_results.not_answered")
16
+ ],
17
+ datasets: [
18
+ {data: [answered_count, not_answered_count]}
19
+ ]
20
+ }
21
+ end
22
+
23
+ def answered_percentage
24
+ (total_answers_files_count[:with_attachments] * 100)/full_questionnaire.total_participants
25
+ end
26
+
27
+ def not_answered_percentage
28
+ (total_answers_files_count[:without_attachments] * 100)/full_questionnaire.total_participants
29
+ end
30
+
31
+ def answered_count
32
+ total_answers_files_count[:with_attachments]
33
+ end
34
+
35
+ def not_answered_count
36
+ total_answers_files_count[:without_attachments]
37
+ end
38
+
39
+ def total_answers_files_count
40
+ total_answers_files_count||= begin
41
+
42
+ total_answers_files = {
43
+ with_attachments: 0,
44
+ without_attachments: 0
45
+ }
46
+
47
+ full_questionnaire.answers.where(question: question).each do |answer|
48
+ if answer.attachments.present?
49
+ total_answers_files[:with_attachments] += 1
50
+ else
51
+ total_answers_files[:without_attachments] += 1
52
+ end
53
+ end
54
+
55
+ total_answers_files
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class FullQuestionnaire
6
+
7
+ delegate :title, to: :questionnaire
8
+
9
+ def initialize(questionnaire)
10
+ @questionnaire= questionnaire
11
+ end
12
+
13
+ def questionnaire
14
+ @questionnaire
15
+ end
16
+
17
+ def questions
18
+ @questions ||= Decidim::Forms::Question.includes([:matrix_rows, :answer_options]).where(questionnaire: questionnaire).order(:position, :id)
19
+ end
20
+
21
+ def pages
22
+ pages = []
23
+ current_page = []
24
+ pages << current_page
25
+
26
+ questions.each do |question|
27
+ if question.question_type != "separator"
28
+ current_page << question
29
+ else
30
+ current_page = []
31
+ pages << current_page
32
+ end
33
+ end
34
+
35
+ pages
36
+ end
37
+
38
+ def participants
39
+ @participants ||= ::Decidim::Forms::QuestionnaireParticipants.new(questionnaire).participants
40
+ end
41
+
42
+ def answers
43
+ @answers ||= Decidim::Forms::Answer.not_separator
44
+ .not_title_and_description
45
+ .joins(:question)
46
+ .where(questionnaire: questionnaire)
47
+ end
48
+
49
+ def total_participants
50
+ @total_participants||= participants.count
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class MatrixQuestionResults < Results
6
+
7
+ def initialize(full_questionnaire, question)
8
+ super(full_questionnaire, question, "matrix_question_results")
9
+ end
10
+
11
+ #-----------------------------------------------
12
+
13
+ private
14
+
15
+ #-----------------------------------------------
16
+
17
+ # labels: data_rows body
18
+ # datasets: An array of hashes with two fields, label and data.
19
+ # Each dataset contains the results of a column in its `data` field.
20
+ # This is, each dataset contains and array with the count of
21
+ # choices on each row of the column.
22
+ #
23
+ # For example, dataset data for column A: [
24
+ # 1, <- one user voted A on row 1
25
+ # 5, <- five users voted A on row 2
26
+ # 3, <- three users voted A on row 3
27
+ # ]
28
+ #
29
+ # NOTE: Correspondence between Decidim models and the matrix
30
+ # question.answer_options -> columns
31
+ # question.matrix_rows -> rows
32
+ # answers -> each is a user response to a question
33
+ # Answer has many choices (AnswerChoices)
34
+ #
35
+ # See the chart expected data here:
36
+ # https://www.chartjs.org/docs/latest/samples/bar/floating.html
37
+ def compute_results
38
+ user_question_answers= full_questionnaire.answers
39
+ choices_sums= Decidim::Forms::AnswerChoice.where("decidim_answer_id IN (#{user_question_answers.select(:id).where(question: question).to_sql})").group(:decidim_question_matrix_row_id, :decidim_answer_option_id).count
40
+
41
+ # labels: appear at the x axis
42
+ # datasets: one dataset for the columns of each row
43
+ data= {
44
+ labels: question.matrix_rows.map {|r| translated_attribute(r.body) },
45
+ datasets: []
46
+ }
47
+ question.answer_options.each do |answer_option|
48
+ dataset = {data: []}
49
+ dataset[:label]= translated_attribute(answer_option.body)
50
+ question.matrix_rows.each do |row|
51
+ key= [row.id, answer_option.id]
52
+ dataset[:data] << (choices_sums[key] || 0)
53
+ end
54
+ data[:datasets] << dataset
55
+ end
56
+
57
+ data
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class OptionsQuestionResults < Results
6
+
7
+ def initialize(full_questionnaire, question)
8
+ super(full_questionnaire, question, "options_question_results")
9
+ end
10
+
11
+ #-----------------------------------------------
12
+
13
+ private
14
+
15
+ #-----------------------------------------------
16
+
17
+ # labels: each anwer_option/column is a label.
18
+ # datasets: Returns only one dataset with the results of each column.
19
+ def compute_results
20
+ user_question_answers= full_questionnaire.answers
21
+ choices_sums= Decidim::Forms::AnswerChoice.where("decidim_answer_id IN (#{user_question_answers.select(:id).where(question: question).to_sql})").group(:decidim_answer_option_id).count
22
+
23
+ x_labels = []
24
+ dataset = {label: I18n.t("survey_results.question_results.number_of_votes"), data: []}
25
+ question.answer_options.map do |answer_option|
26
+ x_labels << translated_attribute(answer_option.body)
27
+
28
+ dataset[:data] << (choices_sums[answer_option.id] || 0)
29
+ end
30
+
31
+ { labels: x_labels, datasets: [dataset] }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class Results
6
+ include Decidim::TranslatableAttributes
7
+
8
+ def self.for(full_questionnaire, question)
9
+ case question.question_type
10
+ when 'single_option', 'multiple_option'
11
+ OptionsQuestionResults.new(full_questionnaire, question)
12
+ when 'short_answer', 'long_answer'
13
+ TextQuestionResults.new(full_questionnaire, question)
14
+ when "files"
15
+ FilesQuestionResults.new(full_questionnaire, question)
16
+ when "matrix_single"
17
+ MatrixQuestionResults.new(full_questionnaire, question)
18
+ when "matrix_multiple"
19
+ MatrixQuestionResults.new(full_questionnaire, question)
20
+ when "sorting"
21
+ SortingQuestionResults.new(full_questionnaire, question)
22
+ when "separator"
23
+ SeparatorQuestionResults.new(full_questionnaire, question)
24
+ end
25
+ end
26
+
27
+ def initialize(full_questionnaire, question, partial_name)
28
+ @full_questionnaire= full_questionnaire
29
+ @question= question
30
+ @partial_name= partial_name
31
+ end
32
+
33
+ attr_reader :question, :partial_name
34
+ attr :full_questionnaire
35
+ delegate :question_type, to: :question
36
+
37
+ def x_labels
38
+ results[:labels]
39
+ end
40
+
41
+ def datasets
42
+ results[:datasets]
43
+ end
44
+
45
+ # A Hash with the labels and datasets resulting from the current Question.
46
+ # In the format expected by Charts.js in the `data` json field.
47
+ #
48
+ # labels: main labels of the chart.
49
+ # datasets: data to be plotted.
50
+ def results
51
+ @results||= compute_results
52
+ end
53
+
54
+ def compute_results
55
+ fail NotImplementedError, "To be implemented by subclasses"
56
+ end
57
+
58
+ def total_answers
59
+ @total_answers||= full_questionnaire.answers.where(question: question).count
60
+ end
61
+
62
+ def answers_percentage(data)
63
+ return 0 if total_answers == 0
64
+ (data * 100)/total_answers
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class SeparatorQuestionResults < Results
6
+
7
+ def initialize(full_questionnaire, question)
8
+ super(full_questionnaire, question, "separator_question_results")
9
+ end
10
+
11
+ def compute_results
12
+ {}
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class SortingQuestionResults < Results
6
+
7
+ def initialize(full_questionnaire, question)
8
+ super(full_questionnaire, question, "sorting_question_results")
9
+ end
10
+
11
+ #---------------------------------------------------------------
12
+
13
+ private
14
+
15
+ #---------------------------------------------------------------
16
+
17
+ # labels: I18n position names, same number as available options to order.
18
+ # datasets: Each dataset is an Array where each item is the number of choices for the given option in that position.
19
+ def compute_results
20
+ user_question_answers= full_questionnaire.answers
21
+ choices_sums= Decidim::Forms::AnswerChoice.where("decidim_answer_id IN (#{user_question_answers.select(:id).where(question: question).to_sql})").group(:decidim_answer_option_id, :position).count
22
+
23
+ labels= []
24
+ datasets= []
25
+ positions= 0...question.answer_options.size
26
+ question.answer_options.each_with_index do |answer_option, idx|
27
+ position= idx + 1
28
+ labels << I18n.t("survey_results.question_results.position", position: position)
29
+
30
+ values= positions.map {|n| choices_sums[[answer_option.id, n]] || 0}
31
+ datasets << {label: translated_attribute(answer_option.body), data: values}
32
+ end
33
+
34
+ {labels: labels, datasets: datasets}
35
+ end
36
+
37
+ def sorting_question(question)
38
+ positions = question.answer_options.count
39
+ values = []
40
+ all_data = []
41
+
42
+ counts = []
43
+ question.answer_options.map do |answer_option|
44
+ values << translated_attribute(answer_option.body)
45
+
46
+ answers.where(question: question).each do |answer|
47
+ positions_count = []
48
+ positions.times do |position|
49
+ positions_count << {position: position, position_count: 0}
50
+ end
51
+
52
+
53
+ choice = answer.choices.find_by(decidim_answer_option_id: answer_option.id)
54
+ position_choice = choice.position
55
+ data = {choice: answer_option.id, positions_count: positions_count}
56
+
57
+ positions.times do |position|
58
+ if position_choice == position
59
+ data[:positions_count][position][:position_count] += 1
60
+ end
61
+ end
62
+ all_data << data
63
+ end
64
+ end
65
+
66
+ { positions: positions, values: values, data: data }
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SurveyResults
5
+ class TextQuestionResults < Results
6
+
7
+ def initialize(full_questionnaire, question)
8
+ super(full_questionnaire, question, "text_question_results")
9
+ end
10
+
11
+ def answered_count
12
+ @answered_count ||= @full_questionnaire.answers.where(question: @question).where.not(body: "").count
13
+ end
14
+
15
+ def not_answered_count
16
+ @not_answered_count||= full_questionnaire.total_participants - answered_count
17
+ end
18
+
19
+ def answered_percentage
20
+ @answered_percentage||= begin
21
+ if full_questionnaire.total_participants > 0
22
+ (answered_count * 100)/full_questionnaire.total_participants
23
+ else
24
+ 0
25
+ end
26
+ end
27
+ end
28
+
29
+ def not_answered_percentage
30
+ @not_answered_percentage||= begin
31
+ if full_questionnaire.total_participants > 0
32
+ ((full_questionnaire.total_participants - answered_count) * 100)/full_questionnaire.total_participants
33
+ else
34
+ 0
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+ #--------------------------------------------
41
+
42
+ private
43
+
44
+ #--------------------------------------------
45
+
46
+ # labels: Corresponding translation for "Answered" and "Not Answered"
47
+ # datasets: Returns only one dataset with the results of each column.
48
+ def compute_results
49
+ data= {
50
+ labels: [
51
+ I18n.t("survey_results.question_results.answered"),
52
+ I18n.t("survey_results.question_results.not_answered")
53
+ ],
54
+ datasets: [
55
+ {data: [answered_count, not_answered_count]}
56
+ ]
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(virtual_path: "decidim/forms/questionnaires/show",
4
+ name: "add_button_to_questionnaire_answers",
5
+ original: "f2fe5ee7066f632fb4a930c922c680ddfbdc1520",
6
+ insert_after: "div.card",
7
+ text: "
8
+ <%= link_to I18n.t('survey_results.questionnaire_show.see_results'), decidim_survey_results.survey_results_path(component_id: current_component.id) %>
9
+ ")
@@ -0,0 +1,6 @@
1
+ import "src/decidim/survey_results/application.js";
2
+
3
+ // Images
4
+ require.context("../images", true)
5
+
6
+ import "entrypoints/decidim_survey_results.scss"
@@ -0,0 +1 @@
1
+ @import "stylesheets/decidim/survey_results/survey_results";
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35"><path d="M17.5 35A17.5 17.5 0 1 1 35 17.5 17.52 17.52 0 0 1 17.5 35zm0-33.06A15.56 15.56 0 1 0 33.06 17.5 15.57 15.57 0 0 0 17.5 1.94zm9.5 13.7H8a1 1 0 0 1 0-1.94h19a1 1 0 0 1 0 1.94zm0 3.68H8a1 1 0 0 1 0-1.94h19a1 1 0 0 1 0 1.94zM22.26 23H8a1 1 0 0 1 0-1.94h14.26a1 1 0 0 1 0 1.94z"/></svg>
@@ -0,0 +1,2 @@
1
+ import "../survey_results/chart_helper.js";
2
+ import "../survey_results/charts.js";
@@ -0,0 +1,24 @@
1
+ export function normalizeLabels(labels) {
2
+ labels.forEach((label, index) => {
3
+ if (label.length > 28) {
4
+ let newLabel = splitLargeLabel(label);
5
+ labels[index] = newLabel
6
+ }
7
+ })
8
+
9
+ return labels;
10
+ };
11
+
12
+ function splitLargeLabel(label) {
13
+ let middle = Math.floor(label.length / 2);
14
+ let before = label.lastIndexOf(' ', middle);
15
+ let after = label.indexOf(' ', middle + 1);
16
+
17
+ if (middle - before < after - middle) {
18
+ middle = before;
19
+ } else {
20
+ middle = after;
21
+ }
22
+
23
+ return [label.substr(0, middle), label.substr(middle + 1)];
24
+ }