chart_helpers 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43d806972569afa6e778e30904a3756ba621c48a
4
+ data.tar.gz: 5258e094ae92e87cfc4a91d669a77a6883cbb375
5
+ SHA512:
6
+ metadata.gz: 6806cbadae42b7c92fa56d0836eb8b2dfaf1bc33836264e12c6f7011f482a8eb33ae791d58d1736d8f8a15f7506b7fefcf8303d0b607aa5ef6a9b0b8060b7610
7
+ data.tar.gz: fb9928b71266838263d8867921b3c38c8bceb3bec805025e4887c222b5136be00075d37d609d108ed7a7a5ab6dd8e0b21b64f78a867ba7be392eff49aed5d4ac
@@ -0,0 +1,12 @@
1
+ .byebug_history
2
+ .DS_Store
3
+ /.bundle/
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ /test/reports
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.14.6
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at julian@jnadeau.ca. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ChartHelpers.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'mocha'
8
+ end
@@ -0,0 +1,97 @@
1
+ # Chart Helpers
2
+
3
+ Chart Helpers is a gem that can parse markdown-like text and turn it into SVG files.
4
+
5
+ ![Gantt Chart](https://cloud.githubusercontent.com/assets/3074765/24520143/6a5e0b06-1555-11e7-9ecc-041e7f34a3ef.png)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'chart_helpers'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install charts_helpers
22
+
23
+ ## Usage
24
+
25
+ ### Gantt Chart
26
+
27
+ ```ruby
28
+ require 'charts_helpers'
29
+
30
+ chart = <<EOF
31
+ gantt
32
+ title My Title
33
+ numberFormat .%2f
34
+
35
+ row 0, :group1, 0.000, 0.100
36
+ row 1, :group1, 0.100, 0.200
37
+ row 2, :group1, 0.200, 0.300
38
+ EOF
39
+ ChartHelpers.render_chart(chart, 'my_chart.svg')
40
+ ```
41
+
42
+ ### Flowchart
43
+
44
+ ```ruby
45
+ require 'charts_helpers'
46
+
47
+ chart = <<EOF
48
+ graph
49
+ A-->B
50
+ subgraph Name
51
+ B--text-->C
52
+ end
53
+ EOF
54
+ ChartHelpers.render_chart(chart, 'my_chart.svg')
55
+ ```
56
+
57
+ *Supported arrows:*
58
+
59
+ | Line Type | |
60
+ |-----------------|-------------------------|
61
+ | `A-->B` | Solid arrow |
62
+ | `B---C` | Solid line |
63
+ | `C--text-->D` | Solid arrow with label |
64
+ | `D--text---E` | Solid line with label |
65
+ | `E-.text.->F` | Dotted arrow with label |
66
+ | `F-.->G` | Dotted arrow |
67
+ | `G==>H` | Bold arrow |
68
+ | `H==text===>I` | Bold line with text |
69
+
70
+ ### DOT Format
71
+
72
+ Full support for GraphViz's .dot format
73
+
74
+ ```ruby
75
+ require 'charts_helpers'
76
+
77
+ chart = <<EOF
78
+ graph graphname {
79
+ a -- b -- c;
80
+ b -- d;
81
+ }
82
+ EOF
83
+ ChartHelpers.render_chart(chart, 'my_chart.svg')
84
+ ```
85
+
86
+ ## Development
87
+
88
+ After checking out the repo, run `bin/setup` to install dependencies. It is developed using the ruby version indicated in `dev.yml`. You will also need the homebrew packages listed in `dev.yml`, or the corresponding Linux packages if you are using Linux.
89
+
90
+ Then, run `bin/testunit` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
91
+
92
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
93
+
94
+ ## Contributing
95
+
96
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ChartHelpers. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
97
+
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'chart_helpers'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ if [ $# -eq 0 ]; then
4
+ ruby -I"test" -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }' -- "$@"
5
+ else
6
+ path=$1
7
+ ruby -I"test" -e "require '${path#test/}'" -- "$@"
8
+ fi
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'chart_helpers/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'chart_helpers'
9
+ spec.version = ChartHelpers::VERSION
10
+ spec.authors = ['Julian Nadeau']
11
+ spec.email = ['julian@jnadeau.ca']
12
+
13
+ spec.summary = 'Create SVG and PNG Charts'
14
+ spec.description = 'Create SVG and PNG ChartHelpers. Includes Gantt Charts'
15
+ spec.homepage = "https://github.com/jules2689/chart_helpers"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.14'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'minitest', '~> 5.0'
27
+ spec.add_development_dependency 'byebug'
28
+
29
+ spec.add_dependency 'victor', '0.2.1'
30
+ spec.add_dependency 'ruby-graphviz'
31
+ spec.add_dependency 'ttfunk'
32
+ end
@@ -0,0 +1,3 @@
1
+ dependencies:
2
+ pre:
3
+ - gem install bundler --pre
data/dev.yml ADDED
@@ -0,0 +1,7 @@
1
+ name: charts
2
+
3
+ up:
4
+ - ruby: 2.4.0
5
+ - bundler
6
+ - homebrew:
7
+ - imagemagick
@@ -0,0 +1,78 @@
1
+ require 'chart_helpers/version'
2
+ require 'chart_helpers/gantt_chart'
3
+ require 'chart_helpers/parsers/gantt'
4
+ require 'chart_helpers/parsers/graphviz'
5
+ require 'graphviz'
6
+
7
+ module ChartHelpers
8
+ class << self
9
+ def render_chart(chart, output_file)
10
+ chart_lines = chart.split("\n")
11
+ chart_type = chart_lines.shift.strip
12
+
13
+ case chart_type
14
+ when /\Agantt\Z/
15
+ parse_gantt(chart_lines, output_file)
16
+ when /\Agraph +\w+ +{/, /\Adigraph\s+\w+\s+{/
17
+ g = GraphViz.parse_string(chart, output: 'svg', file: output_file)
18
+ g.output(svg: output_file)
19
+ g
20
+ when /\Agraph/
21
+ parse_graphviz(chart_lines, chart_type.split('graph').last.strip, output_file)
22
+ else
23
+ raise 'Unsupported chart type declared'
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def parse_gantt(lines, output_file)
30
+ title, date_format, number_format, data = ChartHelpers::Parsers::Gantt.parse(lines)
31
+
32
+ min = data.collect { |d| d[:start] }.min
33
+ max = data.collect { |d| d[:end] }.max
34
+ scale = (max - min) / 20
35
+
36
+ gantt = ChartHelpers::GanttChart.new(
37
+ title: title,
38
+ data: data,
39
+ scale: scale,
40
+ date_format: date_format,
41
+ number_format: number_format
42
+ )
43
+ gantt.build(output_file)
44
+ gantt
45
+ end
46
+
47
+ def parse_graphviz(lines, direction, output_file)
48
+ nodes = {}
49
+ parsed_lines, parsed_nodes = ChartHelpers::Parsers::Graphviz.parse(lines)
50
+ graph = GraphViz.new(:G, type: :digraph, rankdir: direction )
51
+
52
+ parsed_lines.each do |group_name, group|
53
+ local_g = if group_name == :default_global_data
54
+ graph
55
+ else
56
+ graph.add_graph("cluster_#{group_name}", label: group_name)
57
+ end
58
+
59
+ group.each do |node_connection|
60
+ opts = { style: node_connection[:style] }
61
+ opts[:label] = node_connection[:line_text] if node_connection[:line_text]
62
+
63
+ # Add nodes if we need to
64
+ nodes[node_connection[:from_node]] ||= local_g.add_nodes(node_connection[:from_node])
65
+ nodes[node_connection[:to_node]] ||= local_g.add_nodes(node_connection[:to_node])
66
+ local_g.add_edges(
67
+ nodes[node_connection[:from_node]],
68
+ nodes[node_connection[:to_node]],
69
+ opts
70
+ )
71
+ end
72
+ end
73
+
74
+ graph.output(svg: output_file)
75
+ graph
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,193 @@
1
+ require 'victor'
2
+ require 'date'
3
+
4
+ module ChartHelpers
5
+ class GanttChart
6
+ DEFAULT = {
7
+ font_size: 12,
8
+ font_family: 'arial',
9
+ font_weight: 'regular',
10
+ title_font_weight: 'bold',
11
+ font_color: '#000',
12
+ row_color: '#9370DB',
13
+ row_height: 30
14
+ }.freeze
15
+ TITLE_TOP_PADDING = 20
16
+ GRID_TOP_PADDING = TITLE_TOP_PADDING + 80
17
+
18
+ attr_reader :data, :title, :scale
19
+
20
+ def initialize(title:, data:, scale:, width: 2000, height: nil, date_format: nil, number_format: nil, options: {})
21
+ @title = title
22
+ @data = data
23
+ @scale = scale
24
+ @date_format = date_format
25
+ @number_format = number_format
26
+ @options = options
27
+
28
+ @max_value = @data.collect { |v| v[:end] }.max
29
+ @max_value = @max_value.to_time if @max_value.is_a?(DateTime)
30
+ @max_value = @max_value.to_f
31
+
32
+ @min_value = @data.collect { |v| v[:start] }.min
33
+ @min_value = @min_value.to_time if @min_value.is_a?(DateTime)
34
+ @min_value = @min_value.to_f
35
+
36
+ # We aren't guaranteed for our min_value to be 0, so base everything off of the difference
37
+ @diff_value = @max_value - @min_value
38
+ @row_height = setting(:row_height)
39
+ @width = width
40
+ # Height should be one "row_height"" for each row + one for the axis + title
41
+ @height = height || @row_height * (data.size + 1) + 100
42
+
43
+ # Cache for text sizes to avoid duplicating work
44
+ @text_sizes = {}
45
+ end
46
+
47
+ def build(output)
48
+ svg = Victor::SVG.new(width: @width, height: @height)
49
+ render_title(svg)
50
+ render_grid(svg)
51
+ svg.g(transform: "translate(0, #{TITLE_TOP_PADDING})") do
52
+ @data.each_with_index { |row, i| render_row(svg, row, i + 1) }
53
+ end
54
+ svg.save(output)
55
+ end
56
+
57
+ private
58
+
59
+ def setting(*keys)
60
+ keys.each do |key|
61
+ res = @options[key]
62
+ return res if res
63
+ end
64
+
65
+ keys.each do |key|
66
+ res = DEFAULT[key]
67
+ return res if res
68
+ end
69
+
70
+ nil
71
+ end
72
+
73
+ def render_title(svg)
74
+ svg.text(
75
+ @title,
76
+ x: '50%',
77
+ y: TITLE_TOP_PADDING,
78
+ font_family: setting(:title_font_family, :font_family),
79
+ font_weight: setting(:title_font_weight, :font_weight),
80
+ font_size: setting(:title_font_size, :font_size),
81
+ fill: setting(:title_font_color, :font_color),
82
+ style: 'text-anchor: middle;'
83
+ )
84
+ end
85
+
86
+ def render_row(svg, row, idx)
87
+ # The width of a particular row can be calculated as a percentage of the difference of the start and end
88
+ # over the maximum end value
89
+ width = ((row[:end] - row[:start]) / @diff_value * @width).ceil
90
+
91
+ # The y position will always be row height * index, plus one for the title
92
+ y_pos = idx * (@row_height + 1)
93
+
94
+ # Express the x position as a width based change
95
+ x_pos = ((row[:start] - @min_value) / @diff_value * @width).round(2)
96
+
97
+ # This rectangle represents the entry in the gantt chart
98
+ svg.rect(
99
+ x: x_pos,
100
+ y: y_pos,
101
+ width: width,
102
+ height: @row_height,
103
+ fill: (row[:row_color] || setting(:line_row_color, :row_color))
104
+ )
105
+
106
+ # This is the text of the entry
107
+ if row[:title]
108
+ font_size = row[:font_size] || setting(:line_font_size, :font_size)
109
+ text_width, text_height = size_of_text(font_size, row[:title])
110
+ text_x_pos = x_pos + width + 10
111
+
112
+ # If the text will go off the end, then put it in front
113
+ text_x_pos = x_pos - text_width - 10 if text_x_pos + text_width >= @width
114
+
115
+ # If the text will start before the chart, make it overlay the rect
116
+ text_x_pos = 10 if text_x_pos < 10
117
+
118
+ svg.text(
119
+ row[:title],
120
+ x: text_x_pos.round(2),
121
+ y: y_pos + (@row_height + text_height) / 2, # Attempt to center on the rect
122
+ font_family: row[:font_family] || setting(:line_font_family, :font_family),
123
+ font_weight: row[:font_weight] || setting(:line_font_weight, :font_weight),
124
+ font_size: font_size,
125
+ fill: row[:font_color] || setting(:line_font_color, :font_color)
126
+ )
127
+ end
128
+ end
129
+
130
+ def render_grid(svg)
131
+ # The number of ticks we want corresponds to the number of times we can fit our scale
132
+ # in our max value.
133
+ ticks = (@diff_value / @scale).ceil
134
+
135
+ # Render a containing element. We translate this down so the title fits.
136
+ svg.g(class: 'grid', transform: "translate(0, #{GRID_TOP_PADDING})") do
137
+ ticks.times do |i|
138
+ x_pos = (100.0 / ticks * i).round(3)
139
+ text = (@scale * i).round(3)
140
+ render_grid_line(svg, "#{x_pos}%", text)
141
+ end
142
+ # Render one more grid line at the end
143
+ x_pos = 100
144
+ text = (@scale * ticks).round(3)
145
+ render_grid_line(svg, "#{x_pos}%", text)
146
+ end
147
+ end
148
+
149
+ def render_grid_line(svg, x_pos, text = nil)
150
+ y_pos = @height - @row_height - GRID_TOP_PADDING # Position at the bottom, that is, the height - height of axis (row height) - grid top padding
151
+ font_size = setting(:tick_font_size, :font_size)
152
+ text_to_render = grid_text(text)
153
+ _, text_height = size_of_text(font_size, text_to_render.to_s)
154
+ svg.g(class: 'tick', style: 'opacity: 1; stroke: lightgrey; shape-rendering: crispEdges; stroke-width: 1px') do
155
+ svg.line(y2: -50, y1: y_pos - text_height, x1: x_pos, x2: x_pos) # -50 to extend above the graph
156
+ svg.text(text_to_render, x: x_pos, y: y_pos, font_size: font_size, style: 'stroke: none; fill: black;') unless text.nil?
157
+ end
158
+ end
159
+
160
+ def grid_text(text)
161
+ text = if @date_format
162
+ Time.at(@min_value + text).to_datetime.strftime(@date_format)
163
+ elsif @number_format
164
+ format(@number_format, text)
165
+ else
166
+ text.to_s
167
+ end
168
+ end
169
+
170
+ def size_of_text(size, string)
171
+ @text_sizes["#{string}-#{size}"] ||= begin
172
+ # This should work on OS X and Ubuntu
173
+ result = estimate_size(size, string)
174
+ width = 0
175
+ height = 0
176
+ width = Regexp.last_match(1).to_f if result =~ /width: ([\d\.]+);/
177
+ height = Regexp.last_match(1).to_f if result =~ /height: ([\d\.]+);/
178
+ [width, height]
179
+ end
180
+ end
181
+
182
+ def estimate_size(size, string)
183
+ # EXAMPLE OUTPUT FOR `my_string` AT SIZE 12
184
+ # 2017-03-30T02:03:01-04:00 0:00.030 0.010u 6.9.8 Annotate convert[1787]: annotate.c/RenderFreetype/1468/Annotate
185
+ # Font /Library/Fonts/Arial.ttf; font-encoding none; text-encoding none; pointsize 12
186
+ # 2017-03-30T02:03:01-04:00 0:00.030 0.010u 6.9.8 Annotate convert[1787]: annotate.c/GetTypeMetrics/888/Annotate
187
+ # Metrics: text: my_string; width: 52; height: 14; ascent: 11; descent: -3; max advance: 24; bounds: 0.390625,-2 5.875,6; origin: 53,0; pixels per em: 12,12; underline position: -4.5625; underline thickness: 2.34375
188
+ # 2017-03-30T02:03:01-04:00 0:00.030 0.010u 6.9.8 Annotate convert[1787]: annotate.c/RenderFreetype/1468/Annotate
189
+ # Font /Library/Fonts/Arial.ttf; font-encoding none; text-encoding none; pointsize 12
190
+ `convert xc: -pointsize #{size} -debug annotate -annotate 0 '#{string}' null: 2>&1`
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,63 @@
1
+ require 'cgi'
2
+
3
+ module ChartHelpers
4
+ module Parsers
5
+ class Gantt
6
+ # This should match "title" :a1, 0.000, 0.001
7
+ GANTT_LINE_REGEX = %r{
8
+ (?<title>.+)
9
+ (?<group>:\w+)
10
+ ,\s+(?<start>[[^,]+]+)
11
+ ,\s+(?<end>[[^,]+]+)
12
+ }x
13
+
14
+ def self.parse(lines)
15
+ title = 'Gantt Chart'
16
+ date_format = nil
17
+ number_format = nil
18
+ data = []
19
+
20
+ while line = lines.shift
21
+ next if line.empty? || line.nil?
22
+ line.strip!
23
+
24
+ case line
25
+ when /\Atitle/
26
+ if title == 'Gantt Chart'
27
+ # Line will be like:
28
+ # `title THIS IS MY TITLE`
29
+ # We would want "THIS IS MY TITLE"
30
+ title = line.split(' ')[1..-1].join(' ')
31
+ else
32
+ data << parse_line(line)
33
+ end
34
+ when /\AdateFormat/
35
+ # Line will be like:
36
+ # `dateFormat s.SSS`
37
+ # We would want "s.SSS"
38
+ date_format = line.split(' ')[1..-1].join(' ')
39
+ when /\AnumberFormat/
40
+ # Line will be like:
41
+ # `numberFormat s.SSS`
42
+ # We would want "s.SSS"
43
+ number_format = line.split(' ')[1..-1].join(' ')
44
+ else
45
+ data << parse_line(line, date: !date_format.nil?)
46
+ end
47
+ end
48
+
49
+ [title, date_format, number_format, data]
50
+ end
51
+
52
+ def self.parse_line(line, date: false)
53
+ match_data = line.match(GANTT_LINE_REGEX)
54
+ start_val, end_val = if date
55
+ [DateTime.parse(match_data[:start]).to_time.to_f, DateTime.parse(match_data[:end]).to_time.to_f]
56
+ else
57
+ [match_data[:start].to_f, match_data[:end].to_f]
58
+ end
59
+ { title: CGI.escapeHTML(match_data[:title]).strip, start: start_val, end: end_val }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,76 @@
1
+ require 'cgi'
2
+
3
+ module ChartHelpers
4
+ module Parsers
5
+ class Graphviz
6
+ def self.parse(lines)
7
+ graphs = { default_global_data: [] }
8
+ nodes = Set.new
9
+ in_subgraph = false
10
+
11
+ while line = lines.shift
12
+ next if line.empty? || line.nil?
13
+ line.strip!
14
+
15
+ case line
16
+ when /\Asubgraph/
17
+ subgraph_title = line.split('subgraph')[1..-1].join('subgraph').strip
18
+ graphs[subgraph_title] ||= []
19
+ current_subgraph = subgraph_title
20
+ when /\Aend/
21
+ raise 'Not in subgraph' unless current_subgraph
22
+ current_subgraph = nil
23
+ else
24
+ line = parse_line(line)
25
+ if current_subgraph
26
+ graphs[subgraph_title] << line
27
+ else
28
+ graphs[:default_global_data] << line
29
+ end
30
+ nodes.add(line[:from_node])
31
+ nodes.add(line[:to_node])
32
+ end
33
+ end
34
+
35
+ raise 'Never finished subgraph' if in_subgraph
36
+ [graphs, nodes]
37
+ end
38
+
39
+ CONNECTOR_REGEX = %r{
40
+ (?<connector>
41
+ --(?<text>\w+)-->|
42
+ -->|
43
+ --(?<text>\w+)---|
44
+ ---|
45
+ -\.(?<text>\w+)\.->|
46
+ -\.->|
47
+ ==(?<text>\w+)===>|
48
+ ==>
49
+ )
50
+ }x
51
+
52
+ def self.parse_line(line, date: false)
53
+ match_data = line.match(CONNECTOR_REGEX)
54
+ text = match_data[:text]
55
+ connector = match_data[:connector]
56
+ parts = line.split(connector)
57
+
58
+ style = if connector.start_with?('==')
59
+ 'bold'
60
+ elsif connector.start_with?('-.')
61
+ 'dotted'
62
+ else
63
+ ''
64
+ end
65
+
66
+ {
67
+ from_node: parts.first,
68
+ to_node: parts.last,
69
+ line_text: match_data[:text],
70
+ connector: connector,
71
+ style: style
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module ChartHelpers
2
+ VERSION = '0.0.2'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chart_helpers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Julian Nadeau
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-09-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: victor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.2.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.2.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-graphviz
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ttfunk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Create SVG and PNG ChartHelpers. Includes Gantt Charts
112
+ email:
113
+ - julian@jnadeau.ca
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
+ - CODE_OF_CONDUCT.md
121
+ - Gemfile
122
+ - README.md
123
+ - Rakefile
124
+ - bin/console
125
+ - bin/setup
126
+ - bin/testunit
127
+ - chart_helpers.gemspec
128
+ - circle.yml
129
+ - dev.yml
130
+ - lib/chart_helpers.rb
131
+ - lib/chart_helpers/gantt_chart.rb
132
+ - lib/chart_helpers/parsers/gantt.rb
133
+ - lib/chart_helpers/parsers/graphviz.rb
134
+ - lib/chart_helpers/version.rb
135
+ homepage: https://github.com/jules2689/chart_helpers
136
+ licenses: []
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.6.13
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: Create SVG and PNG Charts
158
+ test_files: []