chart-candy 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/Gemfile +4 -0
  2. data/LICENSE +7 -0
  3. data/README.md +109 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/chart_candy/base.coffee +16 -0
  6. data/app/assets/javascripts/chart_candy/counter.coffee +50 -0
  7. data/app/assets/javascripts/chart_candy/donut.coffee +162 -0
  8. data/app/assets/javascripts/chart_candy/index.js +4 -0
  9. data/app/assets/javascripts/chart_candy/line.coffee +338 -0
  10. data/app/assets/stylesheets/chart_candy/base.css.scss +66 -0
  11. data/app/assets/stylesheets/chart_candy/counter.css.scss +11 -0
  12. data/app/assets/stylesheets/chart_candy/donut.css.scss +34 -0
  13. data/app/assets/stylesheets/chart_candy/index.css +6 -0
  14. data/app/assets/stylesheets/chart_candy/line.css.scss +107 -0
  15. data/app/controllers/candy_charts_controller.rb +60 -0
  16. data/config/locales/fr.yml +36 -0
  17. data/config/routes.rb +3 -0
  18. data/lib/chart-candy.rb +23 -0
  19. data/lib/chart-candy/authentication.rb +45 -0
  20. data/lib/chart-candy/base_chart.rb +11 -0
  21. data/lib/chart-candy/builder.rb +46 -0
  22. data/lib/chart-candy/builder/base.rb +70 -0
  23. data/lib/chart-candy/builder/counter.rb +12 -0
  24. data/lib/chart-candy/builder/donut.rb +73 -0
  25. data/lib/chart-candy/builder/line.rb +114 -0
  26. data/lib/chart-candy/builder/xls_builder.rb +159 -0
  27. data/lib/chart-candy/engine.rb +5 -0
  28. data/lib/chart-candy/helpers.rb +169 -0
  29. data/lib/chart-candy/implants.rb +10 -0
  30. data/lib/chart-candy/implants/railtie.rb +17 -0
  31. data/spec/chart-candy_spec.rb +11 -0
  32. data/spec/spec_helper.rb +12 -0
  33. data/vendor/assets/javascripts/d3.js +4 -0
  34. metadata +118 -0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in chart-candy.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 De Marque inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ Chart Candy
2
+ ===============
3
+
4
+ Chart Candy use D3.js library to quickly render AJAX charts in your Rails project. In a minimum amount of code, you should have a functional chart, styled and good to go.
5
+
6
+
7
+ Install
8
+ -------
9
+
10
+ ```
11
+ gem install chart-candy
12
+ ```
13
+
14
+ ### Rails 3
15
+
16
+ In your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'chart-candy'
20
+ ```
21
+
22
+ Setup
23
+ -----
24
+
25
+ First you need to link the assets.
26
+
27
+ In your css manifest put : ``` require chart_candy ```
28
+
29
+ In your javascript manifest put : ``` require chart_candy ```
30
+
31
+ In your layout you must link the D3.js library. You can use the following helper method in order to do that.
32
+
33
+ ```erb
34
+ <%= d3_include_tag %>
35
+ ```
36
+
37
+ Usage
38
+ -----
39
+
40
+ Now, you're ready to add some charts to your project! Chart Candy currently offer 3 chart types : **line**, **donut** and **counter**.
41
+
42
+ ### Line Chart
43
+
44
+ #### Data generation
45
+
46
+ You must create a class that will build the JSON data for the chart. We suggest to create a **app/charts** directory in your Rails project to hold all your charts. For example, if
47
+ I want the chart of downloaded books, I would create **app/charts/downloaded_books_chart.rb**.
48
+
49
+ ```ruby
50
+ class DownloadedBooksChart < ChartCandy::BaseChart
51
+ def build(chart)
52
+ downloads = [ {"time"=>Time.now - 4.months, "value"=>69}, {"time"=>Time.now - 3.months, "value"=>74}, {"time"=>Time.now - 2.months, "value"=>83}, {"time"=>Time.now - 1.months, "value"=>84} ]
53
+
54
+ chart.add_x_axis :date, downloads
55
+ chart.add_y_axis :number, downloads
56
+
57
+ chart.add_line 'downloads', downloads
58
+ end
59
+ end
60
+ ```
61
+
62
+ This build method will be call by the AJAX chart.
63
+
64
+
65
+ #### Chart rendering
66
+
67
+ In your view, you call the chart.
68
+
69
+ ```ruby
70
+ <%= line_chart 'downloaded-book' %>
71
+ ```
72
+
73
+ #### Labelling
74
+
75
+ Chart Candy use Rails I18n to manage text. In order to manage the labels on the chart you'll have to create a YAML that looks like that :
76
+
77
+ ```yaml
78
+
79
+ fr:
80
+ chart_candy:
81
+ downloaded_books:
82
+ title: "Downloaded Books from my Library"
83
+
84
+ axis:
85
+ x:
86
+ label: "Period"
87
+ y:
88
+ label: "Quantity"
89
+ lines:
90
+ downloads:
91
+ label: "Quantity of downloads"
92
+ unit: "downloads"
93
+
94
+ ```
95
+
96
+ ### Donut Chart
97
+
98
+ *Coming Soon*
99
+
100
+
101
+ ### Counter
102
+
103
+ *Coming Soon*
104
+
105
+
106
+ Copyright
107
+ ---------
108
+
109
+ Copyright (c) 2012 De Marque inc. See LICENSE for further details.DownloadedBooks
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ require "rspec/core/rake_task"
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ desc "Run all specs in spec directory"
7
+ RSpec::Core::RakeTask.new(:spec) do |spec|
8
+ spec.pattern = "spec/**/*_spec.rb"
9
+ end
10
+
11
+ task :default => :spec
@@ -0,0 +1,16 @@
1
+ class ChartCandy
2
+ constructor: () ->
3
+ @charts = []
4
+
5
+ if @d3IsLoaded()
6
+ $('div[data-chart-candy]').each (i, chart) =>
7
+ switch $(chart).data('chart-candy')
8
+ when 'counter' then @charts.push new ChartCandyCounter $(chart)
9
+ when 'donut' then @charts.push new ChartCandyDonut $(chart)
10
+ when 'line' then @charts.push new ChartCandyLine $(chart)
11
+
12
+ d3IsLoaded: () -> if d3? then true else false
13
+
14
+
15
+
16
+ $ -> new ChartCandy
@@ -0,0 +1,50 @@
1
+ #*************************************************************************************
2
+ # TOCOMMENT
3
+ #*************************************************************************************
4
+ class @ChartCandyCounter
5
+ constructor: (@holder) ->
6
+ @initUpdateDelay()
7
+ @updateChart()
8
+
9
+
10
+ formatNumber: (num) ->
11
+ num = num.toString()
12
+ if num.length > 4
13
+ num.replace(/\B(?=(?:\d{3})+(?!\d))/g, " ")
14
+ else
15
+ num
16
+
17
+
18
+ initUpdateDelay: () ->
19
+ holder = @holder
20
+
21
+ if holder.data('update-delay')
22
+ delay = holder.data('update-delay') * 1000
23
+
24
+ holder.bind('update', () => @updateData())
25
+ window.setInterval((=> holder.trigger('update')), delay)
26
+
27
+
28
+ isSimilarData: (updated) -> @data.data[0].value is updated.data[0].value
29
+
30
+ loadChart: () ->
31
+ content = ''
32
+
33
+ for d in @data.data
34
+ content += '<span class="label ' + d.nature + '">' + d.label + '</span>' if d.label
35
+ content += '<span class="value ' + d.nature + '" style="display:none">' + @formatNumber(d.value) + '</span>'
36
+
37
+ @holder.find('div.templates div.chart').html(content)
38
+
39
+
40
+ loadTemplates: (@data) ->
41
+ @loadChart()
42
+ @holder.find('div.templates span.value').fadeIn(400)
43
+ @holder.css({ height: @holder.find('div.templates').innerHeight() + 'px' })
44
+
45
+
46
+ reloadChart: (d) -> @holder.find('div.templates span.value').fadeOut(400, => @loadTemplates d)
47
+
48
+ updateData: () -> d3.json(@holder.data('url'), (d) => if not @isSimilarData(d) then @reloadChart(d))
49
+
50
+ updateChart: () -> d3.json(@holder.data('url'), (d) => @loadTemplates d)
@@ -0,0 +1,162 @@
1
+ #*************************************************************************************
2
+ # TOCOMMENT
3
+ #*************************************************************************************
4
+ class @ChartCandyDonut
5
+ constructor: (@holder) ->
6
+ @innerRadius = 55
7
+ @labelMargin = 12
8
+ @holeLineHeight = 20
9
+
10
+ @holderChart = @holder.find('div.templates div.chart')
11
+ @holderTable = @holder.find('div.templates div.table')
12
+
13
+ @initTools()
14
+ @initSize()
15
+
16
+ d3.json(@holder.data('url'), (d) => @loadTemplates d)
17
+
18
+
19
+ centerLabel: (d, radius, text, position) ->
20
+ c = @arc.centroid(d)
21
+ x = c[0]
22
+ y = c[1]
23
+ h = Math.sqrt(x * x + y * y)
24
+
25
+ tX = (x / h * radius)
26
+ tY = (y / h * radius)
27
+
28
+ sizeText = if position is 'inside' then String(text).length * 3.5 else 0
29
+
30
+ if tX > 0 then tX -= sizeText else tX += sizeText
31
+
32
+ return "translate(" + tX + "," + tY + ")"
33
+
34
+ drawChart: () ->
35
+ @holderChart.html('')
36
+
37
+ @root = d3.select("##{@holder.attr('id')} div.templates div.chart")
38
+ @chart = @root.append("svg:svg").attr("width", @fullWidth()).attr("height", @fullHeight())
39
+ @chart.data([@data.slices])
40
+
41
+ donut = d3.layout.pie().sort(null)
42
+
43
+ @arc = d3.svg.arc().innerRadius(@radius - @innerRadius).outerRadius(@radius)
44
+ @arcs = @chart.selectAll("g.arc").data(donut.value((d) -> d.value)).enter().append("svg:g").attr("class", "arc").attr("transform", "translate(" + (@radius + @margins[3]) + "," + (@radius + @margins[0]) + ")")
45
+ @arcs.append("svg:path").attr('class', (d, i) -> "slice-#{i+1}" ).attr "d", @arc
46
+
47
+
48
+ drawHole: () ->
49
+ hole = @chart.append("svg:g").attr("class", "hole").attr("transform", "translate(" + (@radius + @margins[3]) + "," + (@radius + @margins[0]) + ")")
50
+
51
+ startY = (@data.hole.length * @holeLineHeight - @holeLineHeight) / -2
52
+
53
+ for content, i in @data.hole
54
+ hole.append("svg:text").attr("class", "label text#{i}").attr("dy", startY + (i * @holeLineHeight)).attr("text-anchor", "middle").text(content)
55
+
56
+
57
+ drawLabels: () ->
58
+ @drawLabel 'label', @radiusLabel, 'outside' if @data.show_label
59
+ @drawLabel 'valuef', (@radiusLabel - @innerRadius/2.5 - 20), 'inside'
60
+
61
+
62
+ drawLabel: (key, radius, position) ->
63
+ @arcs.append("svg:text").attr("transform", (d,i) => @centerLabel(d, radius, @data.slices[i][key], position))
64
+ .attr("dy", ".35em").attr("text-anchor", (d) -> (if (d.endAngle + d.startAngle) / 2 > Math.PI then "end" else "start"))
65
+ .text (d,i) =>
66
+ if @data.slices[i]['percent'] > 3 or position is 'outside'
67
+ (if position is 'inside' then @data.slices[i][key] else @data.slices[i][key])
68
+ else
69
+ ''
70
+
71
+
72
+
73
+ drawTooltip: () -> @tooltip = @root.append("div").attr('class', 'tooltip')
74
+
75
+ fullHeight: () -> @height + @margins[0] + @margins[2]
76
+
77
+ fullWidth: () -> @width + @margins[1] + @margins[3]
78
+
79
+ initSize: () ->
80
+ @margins = for side in ['Top', 'Right', 'Bottom', 'Left'] then Number(@holderChart.css('padding' + side).replace('px', ''))
81
+ @holderChart.css('padding', '0px')
82
+
83
+ @width = @holder.innerWidth() - @margins[1] - @margins[3]
84
+ @height = @holder.innerHeight() - @margins[0] - @margins[2]
85
+ @radius = Math.min(@width, @height) / 2
86
+ @radiusLabel = @labelMargin + @radius
87
+
88
+
89
+ initTools: () ->
90
+ @tools = @holder.find('div.tools')
91
+ @tools.find('div.holder-template').bind('change', (e) => @showTemplate())
92
+
93
+
94
+ loadChart: () ->
95
+ @drawChart()
96
+ @drawHole()
97
+ @drawLabels()
98
+ @drawTooltip()
99
+ @setChartEvents()
100
+
101
+
102
+ loadTable: () ->
103
+ content = '<table><thead><tr><th>' + @data.label + '</th><th>' + @data.value + '</th></thead><tbody>'
104
+ for d,i in @data.slices then content += "<tr><td>#{d.label}</td><td>#{d.value}</td></tr>"
105
+ content += '</tbody><tfoot><tr><td>' + @data.total.label + '</td><td>' + @data.total.value + '</td></tfoot>'
106
+ content += '</table>'
107
+
108
+ @holderTable.html(content)
109
+
110
+
111
+ loadTemplates: (@data) ->
112
+ @updateTitle()
113
+ @loadChart()
114
+ @loadTable()
115
+ @holder.find('div.templates').fadeIn()
116
+ @holder.css({ height: 'auto' })
117
+
118
+
119
+ setChartEvents: () ->
120
+ self = this
121
+
122
+ @arcs.on("mouseover", (d,i) -> self.tooltip.text(self.data.slices[i].tooltip))
123
+ @arcs.on("mousemove", (d,i) -> self.selectSlice(this))
124
+ @arcs.on("mouseout", (d,i) -> self.unselectSlice(this))
125
+
126
+
127
+ selectSlice: (slice) ->
128
+ @root.selectAll('g.arc path').style('opacity', 0.3)
129
+ @root.selectAll('g.arc text').style('opacity', 0.3)
130
+
131
+ d3.select(slice).select('g.arc path').style('opacity', 1)
132
+ d3.select(slice).selectAll('g.arc text').style('opacity', 1)
133
+
134
+ cursorPosition = d3.mouse(d3.select('body').node())
135
+
136
+ @tooltip.style "left", cursorPosition[0] + 10 + 'px'
137
+ @tooltip.style "top", cursorPosition[1] + 10 + 'px'
138
+ @tooltip.style "display", "block"
139
+
140
+
141
+ showTemplate: () ->
142
+ template = @tools.find('div.holder-template div.switch-field input').val()
143
+
144
+ if template is 'chart'
145
+ @holderTable.fadeOut(400, => @holderChart.fadeIn(400))
146
+ else
147
+ @holderChart.fadeOut(400, => @holderTable.fadeIn(400))
148
+
149
+
150
+ updateTitle: () ->
151
+ content = @data.title
152
+ content += '<span class="subtitle">' + @data.period + '</span>' if @data.period
153
+
154
+ @holder.find('h2.title-chart').html(content)
155
+
156
+
157
+ unselectSlice: (slice) ->
158
+ @root.selectAll('g.arc path').style('opacity', 1)
159
+ @root.selectAll('g.arc text').style('opacity', 1)
160
+
161
+ @tooltip.style("display", "none")
162
+
@@ -0,0 +1,4 @@
1
+ //= require ./base
2
+ //= require ./counter
3
+ //= require ./donut
4
+ //= require ./line
@@ -0,0 +1,338 @@
1
+ #*************************************************************************************
2
+ # TOCOMMENT
3
+ #*************************************************************************************
4
+ class @ChartCandyLine
5
+ constructor: (@holder) ->
6
+ @tickSize = 80
7
+ @legendItemMargin = 30
8
+ @legendHeight = 40
9
+ @legendItemIndent = [@legendItemMargin]
10
+ @pointerRadius = 6
11
+ @tooltipPadding = 30
12
+ @tooltipMargin = 15
13
+
14
+ @holderChart = @holder.find('div.templates div.chart')
15
+ @holderTable = @holder.find('div.templates div.table')
16
+
17
+ @initTools()
18
+ @initSize()
19
+
20
+ @loadData @holder.data('url')
21
+ @initUpdateDelay()
22
+
23
+ buildPointer: (line, num) ->
24
+ pointer = @chart.append("svg:g").attr("class", "pointer pointer-#{num}")
25
+ pointer.append('svg:circle').attr('r', @pointerRadius)
26
+
27
+ xData = for d in line.data then new Date(d.x)
28
+ yData = for d in line.data then d.y
29
+ element = d3.select("##{@holder.attr('id')} g.pointer-#{num}")
30
+
31
+ return { xData: xData, yData: yData, element: element }
32
+
33
+
34
+ currentStep: () ->
35
+ path = if @tools.find('div.holder-step select') then 'select' else 'div.select-field input'
36
+
37
+ return @tools.find('div.holder-step ' + path).val()
38
+
39
+
40
+ drawAxis: (orientation, domain, size, nature, maxTicks) ->
41
+ translate = switch orientation
42
+ when 'left' then [0,0]
43
+ when 'bottom' then [0,size]
44
+ when 'right' then [size, 0]
45
+ else [0,0]
46
+
47
+ qteTicks = if orientation is 'bottom' then @width else @height
48
+ qteTicks = Math.round(qteTicks / @tickSize)
49
+ qteTicks = maxTicks if qteTicks > maxTicks
50
+
51
+ padding = if orientation is 'bottom' then 20 else 12
52
+
53
+ formatNumber = d3.format(",.0f") # for formatting integers
54
+ formatCurrency = (d) -> formatNumber(d) + "$"
55
+
56
+ axis = d3.svg.axis()
57
+ axis = axis.scale(domain).tickSize(-size).ticks(qteTicks).orient(orientation).tickSubdivide(1).tickPadding(padding)
58
+
59
+ switch(nature)
60
+ when 'date' then axis = axis.tickFormat d3.time.format("%d-%m")
61
+ when 'money' then axis = axis.tickFormat formatCurrency
62
+
63
+ @chart.append("svg:g").attr("class", orientation + " axis").attr("transform", "translate(#{translate[0]}, #{translate[1]})").call axis
64
+
65
+ #@rotateLabel() if nature is 'date'
66
+
67
+
68
+ drawChart: () ->
69
+ @holderChart.html('')
70
+
71
+ @chart = d3.select("##{@holder.attr('id')} div.templates div.chart").append("svg:svg").append("svg:g").attr('class', 'chart')
72
+ @chart = @chart.attr("width", @width + @margins[1] + @margins[3])
73
+ @chart = @chart.attr("height", @height + @margins[0] + @margins[2])
74
+ @chart = @chart.attr("transform", "translate(" + @margins[3] + "," + @margins[0] + ")")
75
+
76
+ @chart.append('svg:rect').attr('class', 'bg').attr('width', @width).attr('height', @height)
77
+
78
+ return @chart
79
+
80
+
81
+ drawLegend: () ->
82
+ self = this
83
+
84
+ legend = @chart.append("svg:g").attr("class", "legend").attr('width', @width).attr("transform", "translate(1,10)")
85
+ holder = legend.append('svg:rect').attr('width', @width-1).attr('height', @legendHeight)
86
+ holder_id = @holder.attr('id')
87
+
88
+ itemIndent = @legendItemIndent[0]
89
+
90
+ for l,i in @data.lines
91
+ item = legend.append("svg:g").attr("class", "item item-#{i+1}").attr("transform", "translate(#{itemIndent}, 22)").datum({ target: "line-#{i+1}", pointer: "pointer-#{i+1}" })
92
+ item.append('text').attr('transform', "translate(50, 0)").text(l.label)
93
+ item.append('line').attr('x1', 0).attr('y1', -4).attr('x2', @legendHeight).attr('y2', -4)
94
+ item.on('mouseover', (d, i) -> d3.select("##{holder_id} g.#{d.target}").classed('selected', true))
95
+ item.on('mouseout', (d, i) -> d3.select("##{holder_id} g.#{d.target}").classed('selected', false))
96
+ item.on('click', (d, i) -> self.toggleLine(this, d3.select("##{holder_id} g.#{d.target}"), d3.select("##{holder_id} g.#{d.pointer}")))
97
+
98
+ @legendItemIndent[i+1] = item.select('text').node().getBBox().width + 50 + @legendItemMargin if not @legendItemIndent[i+1]
99
+
100
+ itemIndent += @legendItemIndent[i+1]
101
+
102
+
103
+ drawLine: (dataset, num) ->
104
+ # TODO: Code the right Y axis
105
+ axis = if dataset.axis_y is 'left' then @yAxis else @yAxis
106
+
107
+ line = @chart.append('svg:g').attr('class', "line line-#{num}")
108
+
109
+ @drawShape line, 'line', axis, dataset, num
110
+ @drawShape line, 'area', axis, dataset, num if num is 1 and @data.lines.length < 3
111
+
112
+
113
+ drawPointer: () ->
114
+ self = this
115
+
116
+ @pointers = for lineData,i in @data.lines then @buildPointer(lineData, i+1)
117
+
118
+ @chart.on('mousemove', (d) -> self.updatePointers d3.mouse(this)[0])
119
+ @chart.on('mouseout', (d) -> self.hidePointers())
120
+ @chart.on('mouseover', (d) -> self.showPointers())
121
+
122
+
123
+ drawShape: (holder, nature, axis, line, num) ->
124
+ data = @data
125
+ values = for d in line.data then d.y
126
+
127
+ shape = d3.svg[nature]().interpolate("monotone")
128
+ shape = shape.x((d,i) => @xAxis(if data.axis.x.nature is 'date' then new Date(line.data[i].x) else line.data[i].x))
129
+ shape = if nature is 'line' then shape.y((d) -> axis(d)) else shape.y0(@height).y1((d) -> axis(d))
130
+
131
+ class_name = if nature is 'line' then 'stroke' else nature
132
+
133
+ shape = holder.append("svg:path").attr("class", "#{class_name}").attr("clip-path", "url(#clip)").attr("d", shape(values))
134
+
135
+
136
+ drawTooltip: () ->
137
+ @tooltip = @chart.append('svg:g').attr('class', 'tooltip')
138
+ @tooltip.append('svg:rect').attr('rx', 10).attr('ry', 10)
139
+ @tooltip.append('svg:text').text('')
140
+
141
+
142
+ drawXAxis: () ->
143
+ axis = @data.axis.x
144
+ min = axis.min
145
+ max = axis.max
146
+
147
+ @xAxis = if axis.nature is 'date'
148
+ d3.time.scale().domain([new Date(min), new Date(max)]).range([0, @width])
149
+ else
150
+ d3.scale.linear().domain([min, max]).range([0, @width])
151
+
152
+ @drawAxis 'bottom', @xAxis, @height, axis.nature, axis.max_ticks
153
+
154
+
155
+ drawYAxis: (side) ->
156
+ axis = @data.axis.y
157
+ min = axis.min
158
+ max = axis.max
159
+
160
+ if axis.nature is 'date'
161
+ @yAxis = d3.time.scale().domain([min, max]).range([@height, 0])
162
+ else
163
+ upperGap = (Math.round(max / ((@height * 0.75)/@tickSize)))
164
+ @yAxis = d3.scale.linear().domain([min, max+upperGap]).range([@height, 5])
165
+
166
+ @drawAxis side, @yAxis, @width, axis.nature, axis.max_ticks
167
+
168
+ exportXls: () ->
169
+ url = @tools.find('div.holder-export-xls a.button').attr('href') + '&step=' + @currentStep()
170
+
171
+ location.href = url
172
+
173
+ return false
174
+
175
+
176
+ hidePointers: () ->
177
+ for pointer in @pointers then pointer.element.transition().duration(200).style("opacity", 0)
178
+ @tooltip.transition().duration(200).style("opacity", 0)
179
+
180
+
181
+ initSize: () ->
182
+ @margins = for side in ['Top', 'Right', 'Bottom', 'Left'] then Number(@holderChart.css('padding' + side).replace('px', ''))
183
+ @holderChart.css('padding', '0px')
184
+
185
+ @width = @holderChart.innerWidth() - @margins[1] - @margins[3]
186
+ @height = @holderChart.innerHeight() - @margins[0] - @margins[2]
187
+
188
+
189
+ initTools: () ->
190
+ @tools = @holder.find('div.tools')
191
+ @tools.find('div.holder-step div.select-field').bind('change', (e) => @reloadChart())
192
+ @tools.find('div.holder-step select').change (e) => @reloadChart()
193
+ @tools.find('div.holder-template').bind('change', (e) => @showTemplate())
194
+ @tools.find('div.holder-template select').change (e) => @showTemplate()
195
+ @tools.find('div.holder-export-xls a.button').click (e) => @exportXls()
196
+
197
+
198
+ initUpdateDelay: () ->
199
+ holder = @holder
200
+
201
+ if holder.data('update-delay')
202
+ delay = holder.data('update-delay') * 1000
203
+
204
+ holder.bind('update', () => @reloadChart())
205
+ window.setInterval((=> holder.trigger('update')), delay)
206
+
207
+
208
+ loadChart: () ->
209
+ @drawChart()
210
+ @drawXAxis()
211
+ @drawYAxis('left')
212
+ for d, i in @data.lines then @drawLine(d, i+1)
213
+ @drawLegend() if @data.legend
214
+ @drawPointer()
215
+ @drawTooltip()
216
+
217
+ loadData: (url) -> d3.json(url, (d) => if d then @loadTemplates d)
218
+
219
+
220
+ loadTable: () ->
221
+ content = '<table><thead><tr>'
222
+ content += '<th>' + @data.axis.x.label + '</th>'
223
+ for l in @data.lines then content += '<th>' + l.label + '</th>'
224
+ content += '</thead><tbody>'
225
+ for l,i in @data.lines[0].data
226
+ content += "<tr><td>#{l.label_x}</td>"
227
+ for c in @data.lines then content += "<td class=\"" + @data.axis.y.nature + "\">#{c.data[i].y}</td>"
228
+ content += '</tr>'
229
+ content += "</tbody><tfoot><tr>"
230
+ content += '<td>' + @data.lines[0].total.label + '</td>'
231
+ for l in @data.lines then content += '<td class="' + @data.axis.y.nature + '">' + l.total.value + '</td>'
232
+ content += "</tr></tfoot></table>"
233
+
234
+ @holderTable.html(content)
235
+
236
+
237
+ loadTemplates: (@data) ->
238
+ @updateTitle()
239
+ @loadChart()
240
+ @loadTable()
241
+ @holder.find('div.templates').fadeIn()
242
+ @holder.css({ height: 'auto' })
243
+
244
+
245
+ mainAxis: () -> if @data.axis then @data.axis else @data.axis_left
246
+
247
+ reloadChart: () ->
248
+ url = @tools.find('form').attr('action')
249
+ url += '?' if url.indexOf('?') is -1
250
+ url += "&step=#{@currentStep()}"
251
+
252
+ @holder.css({ height: @holder.height() + 'px' })
253
+ @holder.find('div.templates').fadeOut(400, => @loadData(url))
254
+
255
+
256
+ showPointers: () ->
257
+ for pointer in @pointers then pointer.element.transition().duration(200).style("opacity", 1) if not pointer.element.classed('disabled')
258
+ @tooltip.transition().duration(200).style("opacity", 1)
259
+
260
+
261
+ showTemplate: () ->
262
+ if @tools.find('div.holder-template select')
263
+ template = @tools.find('div.holder-template select').val()
264
+ else
265
+ template = @tools.find('div.holder-template div.switch-field input').val()
266
+
267
+ if template is 'chart'
268
+ @holderTable.fadeOut(400, => @holderChart.fadeIn(400))
269
+ else
270
+ @holderChart.fadeOut(400, => @holderTable.fadeIn(400))
271
+
272
+
273
+ toggleLine: (legendItem, target, pointer) ->
274
+ if target.style("opacity") is '1'
275
+ target.transition().duration(400).style("opacity", 0.2)
276
+ pointer.classed('disabled', true)
277
+ pointer.transition().duration(400).style("opacity", 0)
278
+ d3.select(legendItem).classed('disabled', true)
279
+ else
280
+ target.transition().duration(400).style("opacity", 1)
281
+ pointer.classed('disabled', false)
282
+ pointer.transition().duration(400).style("opacity", 1)
283
+ d3.select(legendItem).classed('disabled', false)
284
+
285
+
286
+ updatePointers: (x) ->
287
+ xDataApprox = @xAxis.invert(x)
288
+
289
+ for pointer, p_count in @pointers
290
+ selection = 0
291
+
292
+ for item,i in pointer.xData then selection = i if Math.abs(xDataApprox - item) < Math.abs(xDataApprox - pointer.xData[selection])
293
+
294
+ posX = @xAxis(pointer.xData[selection])
295
+ posY = @yAxis(pointer.yData[selection])
296
+
297
+ pointer.element.attr("transform", "translate(#{posX}, #{posY})")
298
+
299
+ @updateTooltip(selection) if @data.tooltip
300
+
301
+
302
+ updateTitle: () ->
303
+ content = @data.title
304
+ content += '<span class="subtitle">' + @data.period + '</span>' if @data.period
305
+
306
+ @holder.find('h2.title-chart').html(content)
307
+
308
+
309
+ updateTooltip: (selection) ->
310
+ @tooltip.selectAll("text").remove()
311
+
312
+ tText = @tooltip.append('svg:text')
313
+
314
+ for pointer, p_count in @pointers
315
+ if p_count == 0
316
+ posX = @xAxis(pointer.xData[selection])
317
+ posY = @yAxis(pointer.yData[selection])
318
+
319
+ tText.append('svg:tspan').attr('class', 'x').attr('dy', 25).attr('x', 10).text(@data.lines[p_count].data[selection].label_x)
320
+ tText.append('svg:tspan').attr('class', 'y').attr('dy', 25).attr('x', 10).text(@data.lines[p_count].data[selection].label_y)
321
+ else
322
+ tText.append('svg:tspan').attr('class', 'y').attr('dy', 15).attr('x', 10).text(@data.lines[p_count].data[selection].label_y)
323
+
324
+ tooltipSize = tText.node().getBBox()
325
+
326
+ if posX - (tooltipSize.width + @tooltipMargin + @tooltipPadding) < 0
327
+ posX += @tooltipMargin
328
+ else
329
+ posX -= (tooltipSize.width + @tooltipMargin + @tooltipPadding)
330
+
331
+ @tooltip.attr("transform", "translate(#{posX}, #{posY - @tooltipMargin})")
332
+
333
+ @tooltip.selectAll('rect').attr('width', tooltipSize.width + @tooltipPadding).attr('height', tooltipSize.height + @tooltipPadding)
334
+
335
+
336
+ #rotateLabel: () ->
337
+ #@chart.selectAll(".axis text").attr("transform", (d) -> "translate(" + this.getBBox().height*-2 + "," + this.getBBox().height + ")rotate(-45)")
338
+