grapht 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +267 -0
- data/Rakefile +1 -0
- data/bin/grapht +17 -0
- data/example-data/bar_data.json +5 -0
- data/example-data/donut_data.json +5 -0
- data/example-data/grapht_performance_line_data.json +7 -0
- data/example-data/line_data.json +16 -0
- data/grapht.gemspec +24 -0
- data/lib/graph-definitions/bar-horizontal.js +90 -0
- data/lib/graph-definitions/bar-vertical.js +89 -0
- data/lib/graph-definitions/donut.js +47 -0
- data/lib/graph-definitions/line.js +111 -0
- data/lib/grapht.rb +6 -0
- data/lib/grapht/shell.rb +18 -0
- data/lib/grapht/type.rb +5 -0
- data/lib/grapht/version.rb +3 -0
- data/script/grapht.coffee +107 -0
- data/spec/lib/grapht/shell_spec.rb +79 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/support/rspec-prof.rb +8 -0
- data/vendor/d3.min.js +5 -0
- data/vendor/json2.js +489 -0
- metadata +121 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
function(data) {
|
2
|
+
var margin = { top: 50, right: 20, bottom: 30, left: 40 },
|
3
|
+
width = 400,
|
4
|
+
height = 400,
|
5
|
+
svgWidth = width + margin.right + margin.left,
|
6
|
+
svgHeight = height + margin.top + margin.bottom;
|
7
|
+
|
8
|
+
var svg = d3.select("body").append("svg")
|
9
|
+
.style("fill", "none")
|
10
|
+
.attr("width", svgWidth)
|
11
|
+
.attr("height", svgHeight)
|
12
|
+
.attr('viewBox', "0 -10 " +svgWidth+ " " +svgHeight+ "")
|
13
|
+
.append('g')
|
14
|
+
.attr('transform', 'translate(' +margin.left+ ',' +margin.top+ ')')
|
15
|
+
.style("fill", "none");
|
16
|
+
|
17
|
+
// Axis Scales
|
18
|
+
var x = d3.scale
|
19
|
+
.ordinal()
|
20
|
+
.domain(data.map(function(d) { return d.name; }))
|
21
|
+
.rangeRoundBands([0, width], 0.2);
|
22
|
+
|
23
|
+
var y = d3.scale
|
24
|
+
.linear()
|
25
|
+
.domain([0, d3.max(data, function(d) { return d.value; })])
|
26
|
+
.range([height, 0])
|
27
|
+
.nice();
|
28
|
+
|
29
|
+
// Axes
|
30
|
+
var xAxis = d3.svg
|
31
|
+
.axis()
|
32
|
+
.scale(x)
|
33
|
+
.orient('bottom')
|
34
|
+
.ticks(data.length);
|
35
|
+
|
36
|
+
var yAxis = d3.svg
|
37
|
+
.axis()
|
38
|
+
.scale(y)
|
39
|
+
.orient('left')
|
40
|
+
.tickSize(width);
|
41
|
+
|
42
|
+
svg.selectAll('.bar')
|
43
|
+
.data(data)
|
44
|
+
.enter().append('rect')
|
45
|
+
.attr('class', 'bar')
|
46
|
+
.style('fill', '#428bca')
|
47
|
+
.attr('opacity', '0.3')
|
48
|
+
.attr('y', function(d) { return y(d.value); })
|
49
|
+
.attr('x', function(d) { return x(d.name); })
|
50
|
+
.attr('height', function(d) { return height - y(d.value); })
|
51
|
+
.attr('width', x.rangeBand());
|
52
|
+
|
53
|
+
svg.selectAll('.text')
|
54
|
+
.attr('font-family', 'Arial')
|
55
|
+
.selectAll('label')
|
56
|
+
.attr('fill-opacity', '0.3')
|
57
|
+
|
58
|
+
var xAxisElement = svg.append('g')
|
59
|
+
.attr('class', 'x axis')
|
60
|
+
.attr('transform', "translate(0," +height+ ")")
|
61
|
+
.call(xAxis);
|
62
|
+
|
63
|
+
var yAxisElement = svg.append('g')
|
64
|
+
.attr('class', 'y axis')
|
65
|
+
.attr("transform", "translate(" +width+ ",0)")
|
66
|
+
.call(yAxis);
|
67
|
+
|
68
|
+
xAxisElement.selectAll('line')
|
69
|
+
.style('fill', 'none')
|
70
|
+
.attr('stroke', 'black')
|
71
|
+
.attr('stroke-width', '1px')
|
72
|
+
.attr('stroke-opacity', 0.1)
|
73
|
+
.attr('shape-rendering', 'geometricPrecision');
|
74
|
+
|
75
|
+
xAxisElement.selectAll('text')
|
76
|
+
.style('fill', '#333333')
|
77
|
+
.attr('font-family', 'Arial');
|
78
|
+
|
79
|
+
yAxisElement.selectAll('line')
|
80
|
+
.style('fill', 'none')
|
81
|
+
.attr('stroke', 'black')
|
82
|
+
.attr('stroke-width', '1px')
|
83
|
+
.attr('stroke-opacity', 0.1)
|
84
|
+
.attr('shape-rendering', 'geometricPrecision');
|
85
|
+
|
86
|
+
yAxisElement.selectAll('text')
|
87
|
+
.style('fill', '#333333')
|
88
|
+
.attr('font-family', 'Arial');
|
89
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
function(data) {
|
2
|
+
var margin = { top: 20, right: 20, bottom: 10, left: 10 },
|
3
|
+
width = 400,
|
4
|
+
height = 400,
|
5
|
+
svgWidth = width + margin.right + margin.left,
|
6
|
+
svgHeight = height + margin.top + margin.bottom,
|
7
|
+
radius = Math.min(width, height) / 2;
|
8
|
+
|
9
|
+
var svg = d3.select("body").append("svg")
|
10
|
+
.style("fill", "none")
|
11
|
+
.attr("width", svgWidth)
|
12
|
+
.attr("height", svgHeight)
|
13
|
+
.attr('viewBox', "0 -10 " +svgWidth+ " " +svgHeight+ "")
|
14
|
+
.append('g')
|
15
|
+
.attr('transform', 'translate(' +(width/2)+ ',' +(height/2)+ ')')
|
16
|
+
.style("fill", "none");
|
17
|
+
|
18
|
+
var color = d3.scale
|
19
|
+
.ordinal()
|
20
|
+
.range(["#428bca", "#5cb85c", "#5bc0de", "#f0ad4e", "#d9534f"]);
|
21
|
+
|
22
|
+
var arc = d3.svg.arc()
|
23
|
+
.outerRadius(radius - 10)
|
24
|
+
.innerRadius(radius - 70);
|
25
|
+
|
26
|
+
var pie = d3.layout.pie()
|
27
|
+
.sort(null)
|
28
|
+
.value(function(d) { return d.value; });
|
29
|
+
|
30
|
+
var arcElement = svg.selectAll(".arc")
|
31
|
+
.data(pie(data))
|
32
|
+
.enter().append("g")
|
33
|
+
.attr("class", "arc");
|
34
|
+
|
35
|
+
arcElement.append("path")
|
36
|
+
.attr("d", arc)
|
37
|
+
.style("fill", function(d) { return color(d.data.name); });
|
38
|
+
|
39
|
+
arcElement.append("text")
|
40
|
+
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
|
41
|
+
.attr("dy", ".35em")
|
42
|
+
.style('fill', '#FFFFFF')
|
43
|
+
.attr('font-family', 'Arial')
|
44
|
+
.attr('font-weight', 'bold')
|
45
|
+
.style("text-anchor", "middle")
|
46
|
+
.text(function(d) { return d.data.name; });
|
47
|
+
}
|
@@ -0,0 +1,111 @@
|
|
1
|
+
function(data) {
|
2
|
+
var fns = {
|
3
|
+
extentOf: function(data, axis) {
|
4
|
+
var values = d3.values(data),
|
5
|
+
flatVals = Array.prototype.concat.apply([], values),
|
6
|
+
axisVals = flatVals.map(function(o) { return o[axis]; });
|
7
|
+
|
8
|
+
return d3.extent(axisVals);
|
9
|
+
}
|
10
|
+
};
|
11
|
+
|
12
|
+
var margin = { top: 40, right: 80, bottom: 40, left: 40 },
|
13
|
+
width = 400,
|
14
|
+
height = 400,
|
15
|
+
svgWidth = width + margin.right + margin.left,
|
16
|
+
svgHeight = height + margin.top + margin.bottom;
|
17
|
+
|
18
|
+
var svg = d3.select("body").append("svg")
|
19
|
+
.style("fill", "none")
|
20
|
+
.attr("width", svgWidth)
|
21
|
+
.attr("height", svgHeight)
|
22
|
+
.attr('viewBox', "0 -10 " +svgWidth+ " " +svgHeight+ "")
|
23
|
+
.append('g')
|
24
|
+
.attr('transform', 'translate(' +margin.left+ ',' +margin.top+ ')')
|
25
|
+
.style("fill", "none");
|
26
|
+
|
27
|
+
|
28
|
+
var x = d3.scale
|
29
|
+
.linear()
|
30
|
+
.range([0, width])
|
31
|
+
.domain(fns.extentOf(data, 'x')),
|
32
|
+
y = d3.scale
|
33
|
+
.linear()
|
34
|
+
.range([height, 0])
|
35
|
+
.domain(fns.extentOf(data, 'y')),
|
36
|
+
color = d3.scale
|
37
|
+
.ordinal()
|
38
|
+
.range(["#428bca", "#5cb85c", "#5bc0de", "#f0ad4e", "#d9534f"]);
|
39
|
+
|
40
|
+
var xAxis = d3.svg
|
41
|
+
.axis()
|
42
|
+
.scale(x)
|
43
|
+
.orient('bottom')
|
44
|
+
.tickSize(-height),
|
45
|
+
yAxis = d3.svg
|
46
|
+
.axis()
|
47
|
+
.scale(y)
|
48
|
+
.orient('left')
|
49
|
+
.tickSize(width),
|
50
|
+
line = d3.svg
|
51
|
+
.line()
|
52
|
+
.interpolate('basis')
|
53
|
+
.x(function(d) { return x(d.x); })
|
54
|
+
.y(function(d) { return y(d.y); });
|
55
|
+
|
56
|
+
var xAxisElement = svg.append('g')
|
57
|
+
.attr('class', 'x axis')
|
58
|
+
.attr('transform', "translate(0," +height+ ")")
|
59
|
+
.call(xAxis);
|
60
|
+
|
61
|
+
var yAxisElement = svg.append('g')
|
62
|
+
.attr('class', 'y axis')
|
63
|
+
.attr("transform", "translate(" +width+ ",0)")
|
64
|
+
.call(yAxis);
|
65
|
+
|
66
|
+
var lineElements = svg.selectAll('.line')
|
67
|
+
.data(d3.entries(data))
|
68
|
+
.enter().append('g')
|
69
|
+
.attr('class', 'line');
|
70
|
+
|
71
|
+
lineElements.append('path')
|
72
|
+
.attr('class', 'line')
|
73
|
+
.attr('stroke-width', '2px')
|
74
|
+
.attr('opacity', '0.3')
|
75
|
+
.attr('d', function(d) { return line(d.value) })
|
76
|
+
.attr('stroke', function(d) { return color(d.key); });
|
77
|
+
|
78
|
+
lineElements.append('text')
|
79
|
+
.datum(function(d) { return { name: d.key, value: d.value[d.value.length - 1] }; })
|
80
|
+
.attr('transform', function(d) { return "translate(" +x(d.value.x)+ "," +y(d.value.y)+ ")"; })
|
81
|
+
.attr('x', 3)
|
82
|
+
.attr('dy', '.35em')
|
83
|
+
.style('fill', '#333333')
|
84
|
+
.attr('font-family', 'Arial')
|
85
|
+
.text(function(d) { return d.name; });
|
86
|
+
|
87
|
+
// axis styling
|
88
|
+
xAxisElement.selectAll('line')
|
89
|
+
.style('fill', 'none')
|
90
|
+
.attr('stroke', 'black')
|
91
|
+
.attr('stroke-width', '1px')
|
92
|
+
.attr('stroke-opacity', 0.1)
|
93
|
+
.attr('shape-rendering', 'geometricPrecision');
|
94
|
+
|
95
|
+
xAxisElement.selectAll('text')
|
96
|
+
.style('fill', '#333333')
|
97
|
+
.attr('font-family', 'Arial')
|
98
|
+
.attr('dy', '20px');
|
99
|
+
|
100
|
+
yAxisElement.selectAll('line')
|
101
|
+
.style('fill', 'none')
|
102
|
+
.attr('stroke', 'black')
|
103
|
+
.attr('stroke-width', '1px')
|
104
|
+
.attr('stroke-opacity', 0.1)
|
105
|
+
.attr('shape-rendering', 'geometricPrecision');
|
106
|
+
|
107
|
+
yAxisElement.selectAll('text')
|
108
|
+
.style('fill', '#333333')
|
109
|
+
.attr('font-family', 'Arial')
|
110
|
+
.attr('dx', '-10px');
|
111
|
+
}
|
data/lib/grapht.rb
ADDED
data/lib/grapht/shell.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Grapht
|
4
|
+
module Shell
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
CMD = File.join(Grapht::ROOT, 'bin/grapht')
|
8
|
+
ALLOWED_OPTIONS = %w(-f)
|
9
|
+
|
10
|
+
def self.exec(type, json_data, options={})
|
11
|
+
options = *options.select { |k,v| ALLOWED_OPTIONS.include? k }.flatten
|
12
|
+
|
13
|
+
out, err, status = Open3.capture3 CMD, type, *options, stdin_data: json_data
|
14
|
+
raise Grapht::Shell::Error, err unless status.success?
|
15
|
+
out
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/grapht/type.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
page = require('webpage').create()
|
2
|
+
system = require('system')
|
3
|
+
fs = require('fs')
|
4
|
+
thisFile = system.args[0]
|
5
|
+
graphType = system.args[1]
|
6
|
+
graphFormat = false
|
7
|
+
scriptPath = fs.absolute(thisFile)
|
8
|
+
.replace(/[\w\s\-\.]+?\.(js|coffee)$/i, '')
|
9
|
+
vendorPath = "#{scriptPath}../vendor/"
|
10
|
+
defsPath = "#{scriptPath}../lib/graph-definitions/"
|
11
|
+
userDefsPath = system.env['EXT_GRAPHT_DEFINITIONS_HOME']
|
12
|
+
dependencies = ['d3.min.js', 'json2.js']
|
13
|
+
niceDirPathRX = /\/$|$/
|
14
|
+
|
15
|
+
# -----------------------------------------------------------------------------
|
16
|
+
# Helper Functions
|
17
|
+
# -----------------------------------------------------------------------------
|
18
|
+
|
19
|
+
fns =
|
20
|
+
# Logs the supplied message, and optional trace to STDERR and exits the process
|
21
|
+
# with an exit code of 1.
|
22
|
+
logError: (message, trace) ->
|
23
|
+
fs.write '/dev/stderr', "<ERROR> #{message}\n"
|
24
|
+
|
25
|
+
if trace
|
26
|
+
traceString = trace.map (t) -> "\t#{t.file}: on line: #{t.line}"
|
27
|
+
fs.write '/dev/stderr', traceString.join('\n')
|
28
|
+
|
29
|
+
phantom.exit(1)
|
30
|
+
|
31
|
+
# Searchs for a valid graph definition in the supplied definition paths. If
|
32
|
+
# no definition is found, we log an error to STDERR and exit with an exitcode
|
33
|
+
# of 1.
|
34
|
+
findDef: (type, defPaths...) ->
|
35
|
+
for dir in defPaths when dir?
|
36
|
+
dir = dir.replace(niceDirPathRX, '/')
|
37
|
+
path = "#{dir}#{type}.js"
|
38
|
+
return path if fs.exists(path)
|
39
|
+
|
40
|
+
@logError "No graph definition could be found for '#{type}'"
|
41
|
+
|
42
|
+
loadDef: (def) -> fs.read(def)
|
43
|
+
|
44
|
+
# Wraps the supplied graph definition in a function that executes the definition
|
45
|
+
# and returns the resulting content of the document body. This function is intended
|
46
|
+
# to minimize boiler-plate in graph definitions, and reduce the likelihood of user
|
47
|
+
# error.
|
48
|
+
wrapDef: (def) ->
|
49
|
+
"function() {
|
50
|
+
(#{def}).apply(this, arguments);
|
51
|
+
return document.body.innerHTML;
|
52
|
+
}"
|
53
|
+
|
54
|
+
getOptions: ->
|
55
|
+
optionsIn = system.args[2..]
|
56
|
+
optionsOut = {}
|
57
|
+
slice = 2
|
58
|
+
|
59
|
+
for i in [0...optionsIn.length] by slice
|
60
|
+
[key, value] = optionsIn[i...i+slice]
|
61
|
+
optionsOut[key] = value
|
62
|
+
|
63
|
+
optionsOut
|
64
|
+
|
65
|
+
getFormat: ->
|
66
|
+
options = @getOptions()
|
67
|
+
options['-f'] || options['--format']
|
68
|
+
|
69
|
+
readDataIn: ->
|
70
|
+
try
|
71
|
+
if fs.size('/dev/stdin') == 0
|
72
|
+
fns.logError('No graph data was received!')
|
73
|
+
else
|
74
|
+
fs.read('/dev/stdin')
|
75
|
+
|
76
|
+
catch err
|
77
|
+
@logError err
|
78
|
+
|
79
|
+
|
80
|
+
# -----------------------------------------------------------------------------
|
81
|
+
# Core Graph Generation Logic
|
82
|
+
# -----------------------------------------------------------------------------
|
83
|
+
|
84
|
+
# Configure the page context.
|
85
|
+
page.libraryPath = vendorPath
|
86
|
+
page.onError = fns.logError
|
87
|
+
dependencies.forEach (dp) -> page.injectJs(dp) || Helper.logError "could not load #{dp}!"
|
88
|
+
|
89
|
+
# load and evaluate the graph definition within the context of the JSON, supplied
|
90
|
+
# via STDIN.
|
91
|
+
graphData = fns.readDataIn()
|
92
|
+
graphDef = fns.wrapDef fns.loadDef fns.findDef(graphType, userDefsPath, defsPath)
|
93
|
+
graphFormat = fns.getFormat()
|
94
|
+
parsedData = try
|
95
|
+
JSON.parse(graphData)
|
96
|
+
catch err
|
97
|
+
fns.logError(err)
|
98
|
+
|
99
|
+
page.content = content = page.evaluate(graphDef, parsedData)
|
100
|
+
|
101
|
+
# Write resulting content to STDOUT and exit.
|
102
|
+
if graphFormat
|
103
|
+
page.render '/dev/stdout', { format: graphFormat, quality: 100 }
|
104
|
+
else
|
105
|
+
fs.write '/dev/stdout', "#{content}\n"
|
106
|
+
|
107
|
+
phantom.exit()
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grapht::Shell do
|
4
|
+
describe '.exec' do
|
5
|
+
let(:type) { Grapht::Type::BAR_HORIZONTAL }
|
6
|
+
let(:format) { "" }
|
7
|
+
let(:json_data) {
|
8
|
+
<<-json
|
9
|
+
[
|
10
|
+
{ "name": "foo", "value": 20 },
|
11
|
+
{ "name": "bar", "value": 40 },
|
12
|
+
{ "name": "baz", "value": 35 }
|
13
|
+
]
|
14
|
+
json
|
15
|
+
}
|
16
|
+
|
17
|
+
subject { Grapht::Shell.exec type, json_data, '-f' => format }
|
18
|
+
|
19
|
+
context 'when given a known type' do
|
20
|
+
it { should match(/^<svg/) }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when given an unknown type' do
|
24
|
+
let(:type) { 'some-invalid-graph-type' }
|
25
|
+
|
26
|
+
it "should raise a Grapht::Shell:Error" do
|
27
|
+
expect { subject }.to raise_error(Grapht::Shell::Error, /No graph definition could be found/)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when well-formed data is provided' do
|
32
|
+
it { should match(/^<svg/) }
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when malformed data is provided' do
|
36
|
+
let(:json_data) { "{} <this is not JSON>" }
|
37
|
+
|
38
|
+
it "should raise a Grapht::Shell::Error" do
|
39
|
+
expect { subject }.to raise_error(Grapht::Shell::Error, /Unable to parse JSON string/)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when no data is provided' do
|
44
|
+
let(:json_data) { "" }
|
45
|
+
|
46
|
+
it "should raise a Grapht::Shell::Error" do
|
47
|
+
expect { subject }.to raise_error(Grapht::Shell::Error, /No graph data was received/)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when no format is provided' do
|
52
|
+
it { should match(/^<svg/) }
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when an unknown format is provided' do
|
56
|
+
let(:format) { 'some-crazy-invalid-format' }
|
57
|
+
it { should be_empty }
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when a binary format is provided" do
|
61
|
+
before { subject.force_encoding('binary') }
|
62
|
+
|
63
|
+
context "when a format of 'png' is provided" do
|
64
|
+
let(:format) { 'png' }
|
65
|
+
it { should start_with("\x89\x50\x4E\x47\x0D\x0A\x1A\x0A".force_encoding('binary')) }
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when a format of 'jpg' is provided" do
|
69
|
+
let(:format) { 'jpg' }
|
70
|
+
it { should start_with("\xFF\xD8\xFF\xE0\x00\x10JFIF\x00".force_encoding('binary')) }
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when a format of 'gif' is provided" do
|
74
|
+
let(:format) { 'gif' }
|
75
|
+
it { should start_with("\x47\x49\x46\x38\x37\x61".force_encoding('binary')) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|