diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 5a8387e4260..09010efb104 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,6 +7,7 @@ //= require jquery_ujs //= require vendor/jquery/jquery-ui-1.10.2.custom //= require vendor/jquery/jquery.player.min +//= require vendor/jquery/magna-charta.min // //= require govuk_toolkit //= require shared_mustache diff --git a/app/assets/javascripts/application/convert-html-pub-charts.js b/app/assets/javascripts/application/convert-html-pub-charts.js new file mode 100644 index 00000000000..d359123f1cd --- /dev/null +++ b/app/assets/javascripts/application/convert-html-pub-charts.js @@ -0,0 +1,5 @@ +$(function() { + $('.js-barchart-table').each(function() { + $.magnaCharta($(this)); + }) +}); diff --git a/app/assets/javascripts/vendor/jquery/magna-charta.min.js b/app/assets/javascripts/vendor/jquery/magna-charta.min.js new file mode 100644 index 00000000000..18f7eef7958 --- /dev/null +++ b/app/assets/javascripts/vendor/jquery/magna-charta.min.js @@ -0,0 +1,4 @@ +/*! Magna Charta - v3.0.0 - 2012-12-04 +* https://github.com/alphagov/magna-charta + */ +(function(e){var t=function(){this.init=function(t,n){var r={outOf:65,applyOnInit:!0,toggleText:"Toggle between chart and table",autoOutdent:!1,outdentAll:!1};this.options=e.extend({},r,n);var i=function(){var e,t=3,n=document.createElement("div"),r=n.getElementsByTagName("i");do n.innerHTML="";while(t<10&&r[0]);return t>4?t:e}();return this.ENABLED=!(i&&i<8),this.$table=t,this.$graph=e("
").attr("aria-hidden","true"),this.$graph.attr("class",this.$table.attr("class")).addClass("mc-chart"),this.options.stacked=this.$table.hasClass("mc-stacked"),this.options.negative=this.$table.hasClass("mc-negative"),this.options.multiple=!this.options.stacked&&(this.$table.hasClass("mc-multiple")||this.$table.find("tbody tr").first().find("td").length>2),this.options.autoOutdent=this.options.autoOutdent||this.$table.hasClass("mc-auto-outdent"),this.options.outdentAll=this.options.outdentAll||this.$table.hasClass("mc-outdented"),this.options.multiple&&this.$graph.addClass("mc-multiple"),this.options.hasCaption=!!this.$table.find("caption").length,this.ENABLED&&(this.apply(),this.options.applyOnInit||this.toggle()),this}};t.prototype.construct={},t.prototype.construct.thead=function(){var t=e("
",{"class":"mc-thead"}),n=e("
",{"class":"mc-tr"}),r="";return this.$table.find("th").each(function(t,n){r+='
',r+=e(n).html(),r+="
"}),n.append(r),t.append(n),t},t.prototype.construct.tbody=function(){var t=e("
",{"class":"mc-tbody"}),n="";return this.$table.find("tbody tr").each(function(n,r){var i=e("
",{"class":"mc-tr"}),s="";e(r).find("td").each(function(t,n){s+='
',s+=e(n).html(),s+="
"}),i.append(s),t.append(i)}),t},t.prototype.construct.caption=function(){var e=this.$table.find("caption");return e.clone()},t.prototype.construct.toggleLink=function(){var t=this;return e("",{href:"#","class":"mc-toggle-link",text:this.options.toggleText}).on("click",function(e){t.toggle(e)})},t.prototype.constructChart=function(){var e=this.construct.thead.call(this),t=this.construct.tbody.call(this),n=this.construct.toggleLink.call(this);if(this.options.hasCaption){var r=this.construct.caption.call(this);this.$graph.append(r)}this.$table.before(n),this.$graph.append(e),this.$graph.append(t)},t.prototype.apply=function(){this.ENABLED&&(this.constructChart(),this.addClassesToHeader(),this.calculateMaxWidth(),this.applyWidths(),this.insert(),this.$table.addClass("visually-hidden"),this.applyOutdent())},t.prototype.toggle=function(e){this.$graph.toggle(),this.$table.toggleClass("visually-hidden"),e&&e.preventDefault()},t.prototype.utils={isFloat:function(e){return!isNaN(parseFloat(e))},stripValue:function(e){var t=new RegExp("\\,|£|%|[a-z]","gi");return e.replace(t,"")},returnMax:function(e){var t=0;for(var n=0;nt&&(t=e[n]);return t},isNegative:function(e){return e<0}},t.prototype.addClassesToHeader=function(){var t=this.$graph.find(".mc-th").filter(":not(:first)");this.options.stacked&&(t.last().addClass("mc-stacked-header mc-header-total"),t=t.filter(":not(:last)")),t.addClass("mc-key-header").filter(":not(.mc-stacked-header)").each(function(t,n){e(n).addClass("mc-key-"+(t+1))})},t.prototype.calculateMaxWidth=function(){var t=this,n=[],r=0;this.$graph.find(".mc-tr").each(function(i,s){var o=e(s),u=o.find(".mc-td:not(:first)");if(t.options.stacked){var a=u.last().addClass("mc-stacked-total");u=u.filter(":not(:last)")}o.find(".mc-td:first").addClass("mc-key-cell");var f=0;u.each(function(i,s){var o=e(s).addClass("mc-bar-cell").addClass("mc-bar-"+(i+1)),u=t.utils.stripValue(o.text());if(t.utils.isFloat(u)){var a=parseFloat(u,10),l=Math.abs(a);a===0&&o.addClass("mc-bar-zero"),t.options.negative&&(t.utils.isNegative(a)?(o.addClass("mc-bar-negative"),l>r&&(r=l)):o.addClass("mc-bar-positive")),a=l,t.options.stacked?f+=a:(f=a,n.push(a))}}),t.options.stacked&&n.push(f)});var i={};return i.max=parseFloat(t.utils.returnMax(n),10),i.single=parseFloat(this.options.outOf/i.max,10),this.options.negative&&(i.marginLeft=parseFloat(r,10)*i.single,i.maxNegative=parseFloat(r,10)),i},t.prototype.applyWidths=function(){this.dimensions=this.calculateMaxWidth();var t=this;this.$graph.find(".mc-tr").each(function(n,r){var i=e(r),s=i.find(".mc-bar-cell:not(.mc-bar-zero)").length;i.find(".mc-bar-cell").each(function(n,r){var i=e(r),s=parseFloat(t.utils.stripValue(i.text()),10),o=s*t.dimensions.single,u=Math.abs(s),a=Math.abs(o);if(t.options.negative)if(i.hasClass("mc-bar-positive"))i.css("margin-left",t.dimensions.marginLeft+"%");else if(u"),i.css("width",a+"%")})})},t.prototype.insert=function(){this.$table.after(this.$graph)},t.prototype.applyOutdent=function(){var t=this,n=this.$graph.find(".mc-bar-cell");this.$graph.find(".mc-bar-cell").each(function(n,r){var i=e(r),s=parseFloat(t.utils.stripValue(i.text()),10),o=i.children("span"),u=o.width()+10,a=i.width(),f=parseFloat(i[0].style.width,10),l=i.height();t.options.stacked?u>a&&s>0&&i.addClass("mc-value-overflow"):(s===0&&i.addClass("mc-bar-outdented"),t.options.autoOutdent&&u>a||t.options.outdentAll?(i.addClass("mc-bar-outdented"),o.css({"margin-left":"100%",display:"inline-block"})):i.addClass("mc-bar-indented"))})},e.magnaCharta=function(e,n){return(new t).init(e,n)}})(jQuery); \ No newline at end of file diff --git a/app/assets/stylesheets/frontend/helpers/_magna-charta.scss b/app/assets/stylesheets/frontend/helpers/_magna-charta.scss new file mode 100644 index 00000000000..4c3fe3f9707 --- /dev/null +++ b/app/assets/stylesheets/frontend/helpers/_magna-charta.scss @@ -0,0 +1,287 @@ +/* + * magna-charta example stylesheet + * https://github.com/alphagov/magna-charta + * + * Author: Tim Paul + * Licensed under the MIT license. + */ + + +// DEFAULT CHART STYLES + +$label-width: 40%; // Horizontal width taken up by the bar labels +$label-align: right; // Label alignment +$base-unit: 16px; // For font sizes and related spacing +$compact-font-size: 12px; // For the 'compact' bar style +$bar-padding: 8px; // The padding between the bar values and the edge of the bar +$bar-spacing: 5px; // The spacing around the bars +$stack-spacing: 1px; // For stacked bars, the spacing between each stack in a bar +$caption-side: top; // Caption alignment. Top or bottom. +$key-width: 160px; // Only used by IE. Other browsers get smallest width that fits content + +// CHART COLOUR SCHEME + +@import 'colours'; // Cabinet Office colour palette (feel free to choose your own) +$chart-border: $white; // Chart border colour +$key-border: $white; // Key border colour +$bar-colours: $light-blue, +$turquoise, +$green, +$grass-green, +$yellow, +$orange, +$red, +$mellow-red; + +// HELPER MIXINS + +@mixin visually-hidden{ + // Hides content visually, but not from screen readers + position: absolute !important; + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + padding:0 !important; + border:0 !important; + height: 1px !important; + width: 1px !important; + overflow: hidden; +} + + +// CHART STYLES +.mc-chart { + font-size: $base-unit; + display: table; + width: 100%; + border-spacing: 0 $bar-spacing; + border: 1px solid $chart-border; + padding: $base-unit 0; + position: relative; + margin-bottom: $base-unit; + + // CAPTION STYLES + .mc-caption{ + display: table-caption; + font-size: $base-unit; + text-align: center; + caption-side: $caption-side; + margin: $base-unit 0; + } + + // KEY STYLES + .mc-thead { + background-color: #fff; + display: table-header-group; + padding: $bar-spacing; + border: 1px solid $key-border; + .mc-th{ + display: none; // Hide all header cells first + } + .mc-key-header{ + display: block; // Show the header cells that form part of the key + margin-bottom: $bar-spacing; + margin-left: 100%; + padding-left: $bar-spacing; + border-left: $base-unit solid; + text-align: left; + font-weight: normal; + width: 100%; + &:last-child{ + margin-bottom: 0; + } + } + // Key colours + @for $i from 0 to length($bar-colours) { + .mc-key-#{$i+1} { + border-left-color: nth($bar-colours, $i+1); + } + } + } + // Right and left aligned keys + &.right-key .mc-thead, + &.left-key .mc-thead{ + position: absolute; + top: 100px; + .mc-key-header{ + margin-left: 0; + } + } + &.right-key .mc-thead{ + right: 26px; + } + &.left-key .mc-thead{ + left: 26px; + } + // Hidden keys + &.no-key .mc-thead{ + display: none; + } + + // BAR STYLES + .mc-tbody{ + display: table-row-group; + .mc-tr{ + display: table-row; + // Bars and bar labels + .mc-bar-cell, + .mc-key-cell{ + padding-right: $bar-spacing; + margin-right: $stack-spacing; + box-sizing: border-box; + border: 0 solid; + } + // Bars + .mc-bar-cell { + display: block; + text-align: left; + padding: $bar-padding 0; + margin-bottom: 1px; + color: $white; + text-indent: $bar-padding; + } + // Bar colours + @for $i from 0 to length($bar-colours) { + .mc-bar-#{$i+1} { + background-color: nth($bar-colours, $i + 1); + } + } + // Negative bars + .mc-bar-negative { + text-align: right; + padding-right: $bar-padding; + } + .mc-value-overflow { + text-indent: -99999px !important; + } + // Bar labels + .mc-key-cell { + width: $label-width; + text-indent: 26px; + text-align: $label-align; + display: table-cell; + vertical-align: middle; + } + } + } + + + // STACKED CHARTS + &.mc-stacked { + .mc-stacked-header{ + display: none; // Hide the header for the totals column + } + .mc-th:nth-last-child(2){ + margin-bottom: 0; + } + .mc-stacked-total{ + padding: $bar-padding 0; + background-color: transparent !important; + color: $black; + display: inline-block; + text-indent: 5px; + } + .mc-tbody .mc-bar-cell{ + display: inline-block; + } + } + + // COMPACT CHARTS + &.compact{ + .mc-td.mc-bar-cell{ + font-size: $compact-font-size; + text-indent: 2px; + padding: 0; + } + } + + + // Charts with multiple columns get different coloured bars + $len: length($bar-colours); + @for $i from 0 to $len { + .mc-tr .mc-td.mc-bar-cell.mc-bar-#{$i+1} { + background-color: nth($bar-colours, $i + 1); + } + .mc-tr .mc-th.mc-key-#{$i+1} { + border-left-color: nth($bar-colours, $i+1); + } + + } + + .mc-td, .mc-th { + padding: 0; + padding-right: $bar-spacing; + margin-right: $stack-spacing; + border: 0 solid; + &.mc-bar-cell { + display: block; + background-color: #ebebeb; // Just for testing + text-align: left; + padding: $bar-padding 0; + margin-bottom: 1px; + color: $white; + text-indent: 4px; + } + } + .mc-bar-outdented { + span { color: #111; } + } + caption{ + caption-side: $caption-side; + } + .mc-td.mc-key-cell { + width: $label-width; + text-indent: 26px; + text-align: $label-align; + display: table-cell; + vertical-align: middle; + } + // OUTDENTED BAR VALUES + &.mc-outdented{ + .mc-tbody{ + .mc-tr{ + .mc-bar-cell{ + color: #111; + } + .mc-bar-negative { + text-align: left; + } + } + } + } + + &.mc-negative .mc-td.mc-key-cell { + padding-right: 25px; + } + +} + + +.mc-outdented .mc-bar-cell.mc-bar-negative { + text-align: left; +} + +// .mc-bar-cell.mc-bar-negative.mc-bar-indented { +// position: relative; +// span { +// position: absolute; +// right: 4px; +// } +// } + +.mc-chart.mc-negative .mc-td.mc-key-cell { + padding-right: 25px; +} +.mc-toggle-link{ + display: block; + margin-top: 30px; +} + + +// Hides the original table +.visually-hidden, +.visually-hidden caption{ + @include visually-hidden; + // It's reapplied to captions because Firefox can't hide + // table captions unless it's applied directly to it. Go figure. + +} diff --git a/app/assets/stylesheets/frontend/html-publication.scss b/app/assets/stylesheets/frontend/html-publication.scss index da4b233d079..d8a18d0f85c 100644 --- a/app/assets/stylesheets/frontend/html-publication.scss +++ b/app/assets/stylesheets/frontend/html-publication.scss @@ -39,6 +39,9 @@ // helpers shared with frontend and admin @import "../shared/helpers/_player-container.scss"; + + // Support for bar charts + @import "helpers/_magna-charta.scss"; } diff --git a/app/helpers/govspeak_helper.rb b/app/helpers/govspeak_helper.rb index c2c7254d449..4d40c3d53ca 100644 --- a/app/helpers/govspeak_helper.rb +++ b/app/helpers/govspeak_helper.rb @@ -85,6 +85,7 @@ def bare_govspeak_to_html(govspeak, images = [], options = {}, &block) # pre-processors govspeak = remove_extra_quotes_from_blockquotes(govspeak) govspeak = render_embedded_contacts(govspeak, options[:contact_heading_tag]) + govspeak = set_classes_for_charts(govspeak) markup_to_nokogiri_doc(govspeak, images).tap do |nokogiri_doc| # post-processors @@ -106,6 +107,27 @@ def render_embedded_contacts(govspeak, heading_tag) end end + def set_classes_for_charts(govspeak) + return govspeak if govspeak.blank? + + govspeak.gsub(/{barchart(.*)/) do + stacked = '.mc-stacked' if $1.include? 'stacked' + compact = '.compact' if $1.include? 'compact' + negative = '.mc-negative' if $1.include? 'negative' + + [ + '{:', + '.js-barchart-table', + stacked, + compact, + negative, + '.left-key', + '.mc-auto-outdent', + '}' + ].join(' ') + end + end + def replace_internal_admin_links_in(nokogiri_doc) nokogiri_doc.search('a').each do |anchor| next unless is_internal_admin_link?(uri = anchor['href']) diff --git a/app/views/admin/editions/_govspeak_help.html.erb b/app/views/admin/editions/_govspeak_help.html.erb index 52427ab9cd9..da3c5e7569a 100644 --- a/app/views/admin/editions/_govspeak_help.html.erb +++ b/app/views/admin/editions/_govspeak_help.html.erb @@ -90,6 +90,51 @@ Don't use a single # - this is H1 and is for the title only | banana | yellow |
+

Charts

+
+

Numeric data can be shown as a simple bar chart. Grouped bars are used for multiple columns

+
+    | Department   | Short Speeches | Long Speeches |
+    |--------------|----------------|---------------|
+    | Department 1 | 6              | 6             |
+    | Department 2 | 6              | 8             |
+    | Department 3 | 18             | 2             |
+    {barchart}
+  
+

Stacked bars can be created. The final column is used to display the total.

+
+    | Department | Short Speeches | Long Speeches | Total |
+    |------------|----------------|---------------|-------|
+    | Dept 1     | 6              | 6             | 12    |
+    | Dept 2     | 6              | 8             | 14    |
+    | Dept 3     | 18             | 2             | 20    |
+    {barchart stacked}
+  
+

Large graphs can be displayed compactly to save space.

+
+    | Department   | Short Speeches | Long Speeches |
+    |--------------|----------------|---------------|
+    | Department 1 | 6              | 6             |
+    | Department 2 | 6              | 8             |
+    | Department 3 | 18             | 2             |
+    | Department 4 | 5              | 4             |
+    | Department 4 | 7              | 7             |
+    | Department 6 | 11             | 1             |
+    {barchart compact}
+  
+

If you include negative values, you must indicate this.

+
+    | Department   | Change in Number of Buildings |
+    |--------------|-------------------------------|
+    | Department 1 | -12                           |
+    | Department 1 | 3                             |
+    | Department 1 | -4                            |
+    | Department 1 | 2                             |
+    {barchart negative}
+  
+

All effects can be combined.

+
+

Call to action

$CTA
diff --git a/test/unit/helpers/govspeak_helper_test.rb b/test/unit/helpers/govspeak_helper_test.rb
index 3e335a21ee7..45d2ec37d51 100644
--- a/test/unit/helpers/govspeak_helper_test.rb
+++ b/test/unit/helpers/govspeak_helper_test.rb
@@ -324,6 +324,28 @@ class GovspeakHelperTest < ActionView::TestCase
     end
   end
 
+  test 'will add a barchart class to a marked table' do
+    input = '
+|col|
+|---|
+|val|
+{barchart}
+'
+    html = govspeak_to_html(input)
+    assert_select_within_html html, "table.js-barchart-table"
+  end
+  
+  test 'will add a stacked, compact, negative barchart class to a marked table' do
+        input = '
+|col|
+|---|
+|val|
+{barchart stacked compact negative}
+'
+    html = govspeak_to_html(input)
+    assert_select_within_html html, "table.mc-stacked.js-barchart-table.mc-negative.compact"
+  end  
+
   private
 
   def internal_preview_host