chart_helpers 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/README.md +97 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/testunit +8 -0
- data/chart_helpers.gemspec +32 -0
- data/circle.yml +3 -0
- data/dev.yml +7 -0
- data/lib/chart_helpers.rb +78 -0
- data/lib/chart_helpers/gantt_chart.rb +193 -0
- data/lib/chart_helpers/parsers/gantt.rb +63 -0
- data/lib/chart_helpers/parsers/graphviz.rb +76 -0
- data/lib/chart_helpers/version.rb +3 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/bin/testunit
ADDED
@@ -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
|
data/circle.yml
ADDED
data/dev.yml
ADDED
@@ -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
|
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: []
|