chatchart 1.0.0
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.
- data/LICENSE +3 -0
- data/README +37 -0
- data/demos/demo-animated.rb +118 -0
- data/demos/demo-automatic.rb +40 -0
- data/demos/demo-physics.rb +54 -0
- data/demos/demo-simple.rb +37 -0
- data/lib/chatchart.rb +518 -0
- data/lib/particle.rb +93 -0
- data/lib/point.rb +167 -0
- metadata +63 -0
data/LICENSE
ADDED
data/README
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
ChatChart - ASCII Graph Layout & Drawing
|
2
|
+
|
3
|
+
This is a package of various crappy ruby code which generally
|
4
|
+
revolves around ASCII drawing, graph layout, and physics simulation
|
5
|
+
|
6
|
+
Copyright(c) 2009 by Christopher Abad
|
7
|
+
|
8
|
+
EMAIL: aempirei@gmail.com
|
9
|
+
AIM: ambientempire
|
10
|
+
IRC: aempirei on irc.freenode.net
|
11
|
+
|
12
|
+
http://blog.twentygoto10.com/
|
13
|
+
http://www.twentygoto10.com/
|
14
|
+
http://www.the-mathclub.net/
|
15
|
+
|
16
|
+
this code is licensed under the "don't be a retarded asshole" license.
|
17
|
+
if i don't like how you use this software i can tell you t
|
18
|
+
|
19
|
+
== RUNME ==
|
20
|
+
|
21
|
+
check out the demos in the ./demos directory
|
22
|
+
|
23
|
+
demo-automatic.rb - a simple automatic graph layout sample driver
|
24
|
+
demo-animated.rb - an animated automatic graph layout driver using somewhat of
|
25
|
+
a molecular physics model
|
26
|
+
demo-physics.rb - gravity/mass physics demo
|
27
|
+
demo-simple.rb - simple random graph layout and ascii drawing driver
|
28
|
+
|
29
|
+
== USEME ==
|
30
|
+
|
31
|
+
chatchart.rb - ASCII drawing module
|
32
|
+
point.rb - 2D point class
|
33
|
+
particle.rb - particle class
|
34
|
+
|
35
|
+
== HOWTO ==
|
36
|
+
|
37
|
+
read the samples for now. but i promise i will include more information soon.
|
@@ -0,0 +1,118 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# autograph.rb - Animated Autographer Sample
|
4
|
+
#
|
5
|
+
# this will do some animated automatic graph layout.
|
6
|
+
# make sure your terminal is the right size.
|
7
|
+
# i think the drawing window is set to 70x30
|
8
|
+
#
|
9
|
+
# Copyright(c) 2009 by Christopher Abad
|
10
|
+
# aempirei@gmail.com
|
11
|
+
# http://www.twentygoto10.com/
|
12
|
+
#
|
13
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
14
|
+
# if i don't like how you use this software i can tell you to fuck off
|
15
|
+
# and you can't use it, otherwise you can use it.
|
16
|
+
#
|
17
|
+
# == Usage ==
|
18
|
+
#
|
19
|
+
# autograph.rb [OPTIONS]
|
20
|
+
#
|
21
|
+
# -h, --help:
|
22
|
+
# show help
|
23
|
+
#
|
24
|
+
# -1, --taxicab:
|
25
|
+
# draw using taxicab topology
|
26
|
+
#
|
27
|
+
# -2, --hockey:
|
28
|
+
# draw using hockey-stick topology
|
29
|
+
#
|
30
|
+
|
31
|
+
require 'rubygems'
|
32
|
+
require 'chatchart'
|
33
|
+
require 'getoptlong'
|
34
|
+
require 'rdoc/usage'
|
35
|
+
|
36
|
+
g = ChatChart::Graph.new << [
|
37
|
+
:course - :name0 ,
|
38
|
+
:course - :code ,
|
39
|
+
:course - :'C-I' ,
|
40
|
+
:course - :'S-C' ,
|
41
|
+
:institute - :name1 ,
|
42
|
+
:institute - :'S-I' ,
|
43
|
+
:institute - :'C-I' ,
|
44
|
+
:student - :grade ,
|
45
|
+
:student - :name2 ,
|
46
|
+
:student - :number ,
|
47
|
+
:student - :'S-C' ,
|
48
|
+
:student - :'S-I' ,
|
49
|
+
:a - :b,
|
50
|
+
:a - :c,
|
51
|
+
:a - :d,
|
52
|
+
:b - :q,
|
53
|
+
:b - :w,
|
54
|
+
:b - :e,
|
55
|
+
]
|
56
|
+
|
57
|
+
opts = GetoptLong.new(
|
58
|
+
[ '--help' , '-h', GetoptLong::NO_ARGUMENT ],
|
59
|
+
[ '--hockey' , '-2', GetoptLong::NO_ARGUMENT ],
|
60
|
+
[ '--taxicab', '-1', GetoptLong::NO_ARGUMENT ]
|
61
|
+
)
|
62
|
+
|
63
|
+
linestyle = ChatChart::L1Line
|
64
|
+
|
65
|
+
opts.each do |opt,arg|
|
66
|
+
case opt
|
67
|
+
when '--hockey'
|
68
|
+
linestyle = ChatChart::HLine
|
69
|
+
when '--taxicab'
|
70
|
+
linestyle = ChatChart::L1Line
|
71
|
+
when '--help'
|
72
|
+
RDoc::usage
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
COLS = 70
|
77
|
+
ROWS = 30
|
78
|
+
|
79
|
+
HOME = "\033[H"
|
80
|
+
BOLD = "\033[1m"
|
81
|
+
NORM = "\033[0m"
|
82
|
+
CLRSCR = "\033[2J"
|
83
|
+
|
84
|
+
CURSOR_ON = "\033[?25h"
|
85
|
+
CURSOR_OFF = "\033[?25l"
|
86
|
+
|
87
|
+
WINDOW = [ ChatChart::P[0,0], ChatChart::P[COLS,ROWS] ]
|
88
|
+
|
89
|
+
Kernel::at_exit { puts CURSOR_ON + "\033[50;1H" + NORM }
|
90
|
+
|
91
|
+
Kernel::trap('INT') { |signo| exit }
|
92
|
+
|
93
|
+
puts CURSOR_OFF + CLRSCR
|
94
|
+
|
95
|
+
# loop do
|
96
|
+
|
97
|
+
ChatChart::RandomLayout[ g, COLS, ROWS ]
|
98
|
+
|
99
|
+
C_SZ = 50
|
100
|
+
es = []
|
101
|
+
ses = 0
|
102
|
+
|
103
|
+
loop do
|
104
|
+
ChatChart::SmartLayout[ g, 1 ]
|
105
|
+
es << g.energy
|
106
|
+
if es.length >= C_SZ
|
107
|
+
es = es[-C_SZ,C_SZ]
|
108
|
+
pses = ses
|
109
|
+
ses = es[-C_SZ,C_SZ].inject(0.0) { |sum,e| sum += e }.to_f / C_SZ
|
110
|
+
de = ses - pses
|
111
|
+
else
|
112
|
+
ses = 0
|
113
|
+
de = 0
|
114
|
+
end
|
115
|
+
|
116
|
+
c = g.to_canvas(linestyle) << ChatChart::Title[ "delta-energy: %+.3f" % [ de ], proc { |p| p.r }, 1, 1 ]
|
117
|
+
puts HOME + c.window(*WINDOW)
|
118
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# autograph-simple.rb - Simple AutoGrapher Sample Driver
|
4
|
+
#
|
5
|
+
# Copyright(c) 2009 by Christopher Abad
|
6
|
+
# aempirei@gmail.com
|
7
|
+
# http://www.twentygoto10.com/
|
8
|
+
#
|
9
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
10
|
+
# if i don't like how you use this software i can tell you to fuck off
|
11
|
+
# and you can't use it, otherwise you can use it.
|
12
|
+
#
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'chatchart'
|
16
|
+
|
17
|
+
g = ChatChart::Graph.new << [
|
18
|
+
:course - :name0 ,
|
19
|
+
:course - :code ,
|
20
|
+
:course - :'C-I' ,
|
21
|
+
:course - :'S-C' ,
|
22
|
+
:institute - :name1 ,
|
23
|
+
:institute - :'S-I' ,
|
24
|
+
:institute - :'C-I' ,
|
25
|
+
:student - :grade ,
|
26
|
+
:student - :name2 ,
|
27
|
+
:student - :number ,
|
28
|
+
:student - :'S-C' ,
|
29
|
+
:student - :'S-I' ,
|
30
|
+
:a - :b,
|
31
|
+
:a - :c,
|
32
|
+
:a - :d,
|
33
|
+
:b - :q,
|
34
|
+
:b - :w,
|
35
|
+
:b - :e,
|
36
|
+
]
|
37
|
+
|
38
|
+
puts "be patient, this is ruby code..."
|
39
|
+
ChatChart::SmartLayout[ g ]
|
40
|
+
puts g.to_canvas(ChatChart::L1Line)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# fizzix.rb - Physics Test Code
|
4
|
+
#
|
5
|
+
# Copyright(c) 2009 by Christopher Abad
|
6
|
+
# aempirei@gmail.com
|
7
|
+
# http://www.twentygoto10.com/
|
8
|
+
#
|
9
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
10
|
+
# if i don't like how you use this software i can tell you to fuck off
|
11
|
+
# and you can't use it, otherwise you can use it.
|
12
|
+
#
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'chatchart'
|
16
|
+
require 'particle'
|
17
|
+
|
18
|
+
include ChatChart
|
19
|
+
|
20
|
+
COLS = 100
|
21
|
+
ROWS = 30
|
22
|
+
P_SZ = 10
|
23
|
+
|
24
|
+
HOME = "\033[H"
|
25
|
+
BOLD = "\033[1m"
|
26
|
+
NORM = "\033[0m"
|
27
|
+
|
28
|
+
CURSOR_ON = "\033[?25h"
|
29
|
+
CURSOR_OFF = "\033[?25l"
|
30
|
+
|
31
|
+
WINDOW = [ P[0,0], P[COLS,ROWS] ]
|
32
|
+
|
33
|
+
ps = []
|
34
|
+
|
35
|
+
P_SZ.times { ps << GParticle.new(P.random(COLS, ROWS).vector) }
|
36
|
+
|
37
|
+
c = Canvas.new
|
38
|
+
|
39
|
+
Kernel::at_exit { puts CURSOR_ON + "\033[50;1H" + NORM }
|
40
|
+
Kernel::trap('INT') { |signo| exit }
|
41
|
+
|
42
|
+
puts CURSOR_OFF + BOLD
|
43
|
+
|
44
|
+
loop do
|
45
|
+
ps.each { |p| c << Dot[' ', p.p.quantize] }
|
46
|
+
# calculate the instantaneous acceleration
|
47
|
+
ps.each { |p| p.a = p.n_body_acceleration(ps.reject { |q| q.object_id == p.object_id }) }
|
48
|
+
# update the velocities
|
49
|
+
ps.each { |p| p.v += p.a * GParticle::DT ; p.v *= GParticle::Z }
|
50
|
+
# project new positions
|
51
|
+
ps.each { |p| p.p += p.v * GParticle::DT }
|
52
|
+
ps.each { |p| c << Dot['@', p.p.quantize] }
|
53
|
+
puts HOME + c.window(*WINDOW)
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# sample.rb - ChatChart Sample Driver
|
4
|
+
#
|
5
|
+
# Copyright(c) 2009 by Christopher Abad
|
6
|
+
# aempirei@gmail.com
|
7
|
+
# http://www.twentygoto10.com/
|
8
|
+
#
|
9
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
10
|
+
# if i don't like how you use this software i can tell you to fuck off
|
11
|
+
# and you can't use it, otherwise you can use it.
|
12
|
+
#
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'chatchart'
|
16
|
+
|
17
|
+
COLS = 60
|
18
|
+
ROWS = 14
|
19
|
+
|
20
|
+
g = ChatChart::Graph.new << [
|
21
|
+
:course - :name0 ,
|
22
|
+
:course - :code ,
|
23
|
+
:course - :'C-I' ,
|
24
|
+
:course - :'S-C' ,
|
25
|
+
:institute - :name1 ,
|
26
|
+
:institute - :'S-I' ,
|
27
|
+
:institute - :'C-I' ,
|
28
|
+
:student - :grade ,
|
29
|
+
:student - :name2 ,
|
30
|
+
:student - :number ,
|
31
|
+
:student - :'S-C' ,
|
32
|
+
:student - :'S-I' ,
|
33
|
+
]
|
34
|
+
|
35
|
+
ChatChart::RandomLayout[ g, COLS, ROWS ]
|
36
|
+
|
37
|
+
print g.to_canvas << ChatChart::Title[ 'SHIT FOR BRAINS'.reverse, proc { |p| p.l.d }, -2, 0 ]
|
data/lib/chatchart.rb
ADDED
@@ -0,0 +1,518 @@
|
|
1
|
+
#
|
2
|
+
# ChatChart
|
3
|
+
# chatchart.rb - ASCII Graphs
|
4
|
+
#
|
5
|
+
# use this to create an ascii graph described using a simple array of
|
6
|
+
# vertex adjacencies. just check out the sample driver (sample.rb)
|
7
|
+
#
|
8
|
+
# Copyright(c) 2009 by Christopher Abad
|
9
|
+
# aempirei@gmail.com
|
10
|
+
# http://www.twentygoto10.com/
|
11
|
+
#
|
12
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
13
|
+
# if i don't like how you use this software i can tell you to fuck off
|
14
|
+
# and you can't use it, otherwise you can use it.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'point'
|
18
|
+
require 'particle'
|
19
|
+
|
20
|
+
class Symbol
|
21
|
+
def <=>(b)
|
22
|
+
self.to_s <=> b.to_s
|
23
|
+
end
|
24
|
+
def -(b)
|
25
|
+
ChatChart::Edge[self,b]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Circuit
|
30
|
+
def self.[](p)
|
31
|
+
edges = []
|
32
|
+
p.split(//).each_cons(2) { |a,b| edges << a.to_sym - b.to_sym }
|
33
|
+
edges
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module ChatChart
|
38
|
+
|
39
|
+
# a canvas is the primary drawing space
|
40
|
+
|
41
|
+
class Canvas < Hash
|
42
|
+
def [](p)
|
43
|
+
raise 'non-point' unless p.is_a? P
|
44
|
+
super(p)
|
45
|
+
end
|
46
|
+
|
47
|
+
def <<(g)
|
48
|
+
case g
|
49
|
+
when Proc
|
50
|
+
g.call(self)
|
51
|
+
else
|
52
|
+
raise 'non-curve-proc' unless g < Curve
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def []=(p,v)
|
58
|
+
raise 'non-point' unless p.is_a? P
|
59
|
+
raise 'non-char' unless v.is_a?(String) and v.length == 1
|
60
|
+
super(p,v)
|
61
|
+
end
|
62
|
+
|
63
|
+
def bounds
|
64
|
+
xs = keys.map { |p| p.x }.sort
|
65
|
+
ys = keys.map { |p| p.y }.sort
|
66
|
+
|
67
|
+
return [ P[xs.first,ys.first], P[xs.last,ys.last] ]
|
68
|
+
end
|
69
|
+
|
70
|
+
def window(ul,lr)
|
71
|
+
str = ''
|
72
|
+
ul = ul.quantize
|
73
|
+
lr = lr.quantize
|
74
|
+
((ul.y)..(lr.y)).each do |y|
|
75
|
+
((ul.x)..(lr.x)).each do |x|
|
76
|
+
p = P[x,y]
|
77
|
+
if has_key?(p)
|
78
|
+
str << self[p]
|
79
|
+
else
|
80
|
+
str << ' '
|
81
|
+
end
|
82
|
+
end
|
83
|
+
str << "\n"
|
84
|
+
end
|
85
|
+
return str
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
window *bounds
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# the curve is a general path through the canvas space, which consists of some geometric description
|
94
|
+
# and the [] operator which returns a proc that accepts a single canvas parameter which is then drawn to
|
95
|
+
|
96
|
+
class Curve
|
97
|
+
def self.[](*geometry)
|
98
|
+
proc { |canvas| self.draw(canvas, *geometry) }
|
99
|
+
end
|
100
|
+
def self.draw(canvas, *geometry)
|
101
|
+
raise 'undefined method'
|
102
|
+
end
|
103
|
+
def self.get_point(*geometry)
|
104
|
+
case geometry.first
|
105
|
+
when P
|
106
|
+
geometry.first
|
107
|
+
when Integer
|
108
|
+
P[geometry.first, geometry.last]
|
109
|
+
else
|
110
|
+
raise 'invalid-geometry'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
def self.get_points(*geometry)
|
114
|
+
return [] if geometry.empty?
|
115
|
+
case geometry.first
|
116
|
+
when P
|
117
|
+
[ geometry.shift.dup ] + get_points(*geometry)
|
118
|
+
when Integer
|
119
|
+
[ P[geometry.shift, geometry.shift] ] + get_points(*geometry)
|
120
|
+
else
|
121
|
+
raise 'invalid-geometry'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
def self.get_interval(*geometry)
|
125
|
+
points = get_points(*geometry)
|
126
|
+
points.first .. points.last
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# titles are just an instance of a general curve, where there is
|
131
|
+
# some geometry describing their origin and a block describing
|
132
|
+
# their differential motion
|
133
|
+
|
134
|
+
class Title < Curve
|
135
|
+
def self.[](title, blk, *geometry)
|
136
|
+
proc { |canvas| self.draw(canvas, title, *geometry, &blk) }
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.draw(canvas, title, *geometry, &blk)
|
140
|
+
p = get_point(*geometry).quantize
|
141
|
+
title.to_s.split(//).each do |ch|
|
142
|
+
canvas[p] = ch
|
143
|
+
p = blk.call(p)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class Dot < Curve
|
149
|
+
def self.[](dot, *geometry)
|
150
|
+
proc { |canvas| self.draw(canvas, dot, *geometry) }
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.draw(canvas, dot, *geometry)
|
154
|
+
p = get_point(*geometry)
|
155
|
+
canvas[p] = dot
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# lines are just an instance of a general curve, where there is some geometry describing their path
|
160
|
+
|
161
|
+
class Line < Curve
|
162
|
+
def self.[](*geometry)
|
163
|
+
proc { |canvas| self.draw(canvas, *geometry) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# L1-norm (taxicab norm) lines are drawn up/down then left/right from origin (a) to destination (b)
|
168
|
+
|
169
|
+
class L1Line < Line
|
170
|
+
def self.draw(canvas, *geometry)
|
171
|
+
|
172
|
+
a,b = get_points(*geometry)
|
173
|
+
a.quantize!
|
174
|
+
b.quantize!
|
175
|
+
c = a % b
|
176
|
+
|
177
|
+
y1,y2 = [a.y,c.y].sort
|
178
|
+
|
179
|
+
(y1..y2).each do |y|
|
180
|
+
p = P[c.x,y]
|
181
|
+
next if p == c
|
182
|
+
canvas[p] = '|'
|
183
|
+
end
|
184
|
+
|
185
|
+
x1,x2 = [b.x,c.x].sort
|
186
|
+
|
187
|
+
(x1..x2).each do |x|
|
188
|
+
p = P[x,c.y]
|
189
|
+
next if p == c
|
190
|
+
canvas[p] = '-'
|
191
|
+
end
|
192
|
+
|
193
|
+
canvas[c] = '+'
|
194
|
+
canvas[a] = 'o'
|
195
|
+
canvas[b] = 'o'
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# a hockey line is somewhere between an L1-norm and L2-norm topology where the topology
|
200
|
+
# is constructed from 0, 45, and 90 degree edges with unit projections onto x and y and
|
201
|
+
# where length of a vector v is calculated as follows:
|
202
|
+
# v = <dx,dy>
|
203
|
+
# dz = min(dx,dy)
|
204
|
+
# dw = |dx-dy|
|
205
|
+
# |v| = |<dz,dz>|+dw
|
206
|
+
|
207
|
+
class HLine < Line
|
208
|
+
def self.draw(canvas, *geometry)
|
209
|
+
|
210
|
+
a,b = get_points(*geometry)
|
211
|
+
a.quantize!
|
212
|
+
b.quantize!
|
213
|
+
|
214
|
+
v = b-a
|
215
|
+
|
216
|
+
amstep = (v.proj(P::J).unit + v.proj(P::I).unit)
|
217
|
+
amstep.quantize!
|
218
|
+
am = amstep * v.norm(:min)
|
219
|
+
am.quantize!
|
220
|
+
|
221
|
+
mb = v-am
|
222
|
+
mbstep = mb.unit.quantize
|
223
|
+
|
224
|
+
pos = a
|
225
|
+
|
226
|
+
while pos != a + am
|
227
|
+
pos += amstep
|
228
|
+
canvas[pos] = (amstep.monic == P[1,1]) ? '\\' : '/'
|
229
|
+
end
|
230
|
+
|
231
|
+
while pos != b
|
232
|
+
pos += mbstep
|
233
|
+
canvas[pos] = (mbstep.x == 0.0) ? '|' : '-'
|
234
|
+
end
|
235
|
+
|
236
|
+
canvas[a + am] = '+'
|
237
|
+
canvas[a] = 'o'
|
238
|
+
canvas[b] = 'o'
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class WiggleLine < Line
|
243
|
+
end
|
244
|
+
|
245
|
+
class Edge
|
246
|
+
attr_accessor :from
|
247
|
+
attr_accessor :to
|
248
|
+
|
249
|
+
def self.[](a,b)
|
250
|
+
new(a,b)
|
251
|
+
end
|
252
|
+
|
253
|
+
def initialize(a,b)
|
254
|
+
raise 'invalid-edge' unless a.is_a? Symbol and b.is_a? Symbol
|
255
|
+
@from,@to = [a,b].sort
|
256
|
+
end
|
257
|
+
|
258
|
+
def name
|
259
|
+
"#{from} -- #{to}".to_sym
|
260
|
+
end
|
261
|
+
|
262
|
+
def to_a
|
263
|
+
[from,to]
|
264
|
+
end
|
265
|
+
|
266
|
+
def hash
|
267
|
+
to_a.hash
|
268
|
+
end
|
269
|
+
|
270
|
+
def neighbor(v)
|
271
|
+
v == to ? from : to
|
272
|
+
end
|
273
|
+
|
274
|
+
# check for edge endpoint inclusion of a vertex (or by name of vertex)
|
275
|
+
def ===(v)
|
276
|
+
case v
|
277
|
+
when Array
|
278
|
+
v.inject(false) { |acc,w| acc ||= (self === w) }
|
279
|
+
when Vertex
|
280
|
+
self === v.name
|
281
|
+
when Symbol
|
282
|
+
@from == v or @to == v
|
283
|
+
else
|
284
|
+
raise 'invalid-comparison'
|
285
|
+
end
|
286
|
+
end
|
287
|
+
# compare two edges (undirected)
|
288
|
+
def ==(e)
|
289
|
+
raise 'invalid-comparison' unless e.is_a? Edge
|
290
|
+
from == e.from and to == e.to
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class Vertex
|
295
|
+
attr_reader :p
|
296
|
+
attr_reader :name
|
297
|
+
def initialize(name)
|
298
|
+
raise 'non-symbol' unless name.is_a? Symbol
|
299
|
+
@p = Particle[]
|
300
|
+
@name = name
|
301
|
+
end
|
302
|
+
def initialize_copy(orig)
|
303
|
+
@p = @p.dup
|
304
|
+
end
|
305
|
+
def hash
|
306
|
+
@name.hash
|
307
|
+
end
|
308
|
+
# compare vertices, which allows for comparison against symbols, which is the vertex name type
|
309
|
+
def ===(v)
|
310
|
+
case v
|
311
|
+
when Array
|
312
|
+
v.inject(false) { |acc,w| acc ||= (self === w) }
|
313
|
+
when Symbol
|
314
|
+
self.name == v
|
315
|
+
when Vertex
|
316
|
+
self == v
|
317
|
+
else
|
318
|
+
raise 'invalid-comparison'
|
319
|
+
end
|
320
|
+
end
|
321
|
+
def ==(v)
|
322
|
+
raise 'invalid-comparison' unless v.is_a? Vertex
|
323
|
+
self.name == v.name
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# graphs are collections of vertices and edges without layout information
|
328
|
+
|
329
|
+
class Graph
|
330
|
+
attr_reader :e # edges
|
331
|
+
attr_reader :v # vertices
|
332
|
+
attr_accessor :root # root node name
|
333
|
+
def initialize(root=nil)
|
334
|
+
@e = {}
|
335
|
+
@v = {}
|
336
|
+
@root = case root
|
337
|
+
when Symbol
|
338
|
+
root
|
339
|
+
when Vertex
|
340
|
+
root.name
|
341
|
+
when NilClass
|
342
|
+
# do nothing
|
343
|
+
else
|
344
|
+
raise 'invalid-root-node'
|
345
|
+
end
|
346
|
+
self << root if root
|
347
|
+
end
|
348
|
+
def initialize_copy(orig)
|
349
|
+
@e = @e.dup
|
350
|
+
@v = @v.dup
|
351
|
+
end
|
352
|
+
def <<(elem)
|
353
|
+
case elem
|
354
|
+
when Graph
|
355
|
+
self << elem.v.values << elem.e.values
|
356
|
+
when Array
|
357
|
+
elem.each { |q| self << q }
|
358
|
+
when Edge
|
359
|
+
@e[elem.name] = elem.dup
|
360
|
+
self << elem.from unless @v[elem.from]
|
361
|
+
self << elem.to unless @v[elem.to]
|
362
|
+
when Vertex
|
363
|
+
@v[elem.name] = elem.dup
|
364
|
+
when Symbol
|
365
|
+
self << Vertex.new(elem)
|
366
|
+
when NilClass
|
367
|
+
# do nothing
|
368
|
+
else
|
369
|
+
raise 'non-graph-element'
|
370
|
+
end
|
371
|
+
self
|
372
|
+
end
|
373
|
+
def to_s
|
374
|
+
v_str = @v.keys.map { |s| "\t#{s};\n" }.join
|
375
|
+
e_str = @e.keys.map { |s| "\t#{s};\n" }.join
|
376
|
+
"#{self.class.name.downcase} g {\n#{e_str}#{v_str}};\n"
|
377
|
+
end
|
378
|
+
def to_canvas(linestyle = L1Line)
|
379
|
+
canvas = Canvas.new
|
380
|
+
@e.values.each { |e| canvas << linestyle[ @v[e.from].p.p, @v[e.to].p.p ] }
|
381
|
+
@v.values.each { |v| canvas << Title[ v.name, proc { |p| p.r }, v.p.p ] }
|
382
|
+
canvas
|
383
|
+
end
|
384
|
+
|
385
|
+
def find_edges(*vs)
|
386
|
+
@e.values.select { |e| e === vs }
|
387
|
+
end
|
388
|
+
|
389
|
+
def neighborhood(v)
|
390
|
+
find_edges(v).map { |e| @v[e.neighbor(v)] }
|
391
|
+
end
|
392
|
+
|
393
|
+
def [](*vs)
|
394
|
+
Graph.new(vs.first) << find_edges(*vs)
|
395
|
+
end
|
396
|
+
|
397
|
+
def degree(v)
|
398
|
+
find_edges(v).length
|
399
|
+
end
|
400
|
+
|
401
|
+
def pop(*vs)
|
402
|
+
# pop assumes vertices come in as their name, not a full vertex object
|
403
|
+
k1hood = self[*vs] # get k-1 N
|
404
|
+
vs.each { |v| @v.delete v } # delete k-1 N vertices
|
405
|
+
k1hood.e.keys.each { |e| @e.delete e } # delete k-1 N edges
|
406
|
+
k1hood # return k-1 N
|
407
|
+
end
|
408
|
+
|
409
|
+
def span(plan)
|
410
|
+
|
411
|
+
remaining = dup
|
412
|
+
popped = remaining.pop plan.get_root(remaining)
|
413
|
+
tree = Graph.new << popped.root
|
414
|
+
|
415
|
+
until remaining.v.empty?
|
416
|
+
border = self[*tree.v.keys][*remaining.v.keys] # find edges on the border between the tree and the remaining graph
|
417
|
+
edge = plan.get_edge(border) # choose one edge and new node to add to the tree
|
418
|
+
tree << edge # add to the tree
|
419
|
+
remaining.pop(edge.from, edge.to) # remove k-1 N from remaining graph
|
420
|
+
end
|
421
|
+
|
422
|
+
tree
|
423
|
+
end
|
424
|
+
|
425
|
+
def energy
|
426
|
+
@v.values.inject(0.0) { |sum,v| sum += (v.p.v * v.p.m).norm }
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# edge/node selection plan for things like spanning trees
|
431
|
+
class Plan
|
432
|
+
end
|
433
|
+
|
434
|
+
class FirstPlan < Plan
|
435
|
+
def self.get_edge(graph)
|
436
|
+
graph.e.values.first
|
437
|
+
end
|
438
|
+
def self.get_root(graph)
|
439
|
+
graph.v.keys.first
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# layouts take graphs and output canvases, when then can be printed or whatever
|
444
|
+
|
445
|
+
class Layout
|
446
|
+
def self.[](*as)
|
447
|
+
self.layout(*as)
|
448
|
+
self
|
449
|
+
end
|
450
|
+
def self.layout(*as)
|
451
|
+
raise 'undefined method'
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# random layout places vertices at random and then connects them via L1-norm edges
|
456
|
+
|
457
|
+
class RandomLayout < Layout
|
458
|
+
def self.layout(g,width,height)
|
459
|
+
g.v.values.each { |v| v.p.p.randomize!(width,height) }
|
460
|
+
self
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
class SmartLayout < Layout
|
465
|
+
|
466
|
+
CW_SZ = 200 # convolution window size
|
467
|
+
|
468
|
+
def self.layout(g,iter=nil)
|
469
|
+
|
470
|
+
RandomLayout[g, 40, 24] if iter.nil?
|
471
|
+
|
472
|
+
es = []
|
473
|
+
ps = g.v.values.map { |v| v.p }
|
474
|
+
ses = 0
|
475
|
+
|
476
|
+
while iter.nil? or (iter -= 1) >= 0;
|
477
|
+
|
478
|
+
# update the gravity field
|
479
|
+
ps.each { |p| p.a = p.n_body_acceleration(ps.reject { |q| q.object_id == p.object_id }) }
|
480
|
+
|
481
|
+
# update the bond field
|
482
|
+
g.v.keys.each do |v|
|
483
|
+
p = g.v[v].p
|
484
|
+
qs = g.neighborhood v
|
485
|
+
p.b = p.n_body_bond(qs.map { |q| q.p })
|
486
|
+
end
|
487
|
+
|
488
|
+
# update the velocities
|
489
|
+
ps.each do |p|
|
490
|
+
p.v *= Particle::Z
|
491
|
+
p.v += p.a * Particle::DT
|
492
|
+
p.v += p.b * Particle::DT
|
493
|
+
end
|
494
|
+
|
495
|
+
# project new positions
|
496
|
+
ps.each { |p| p.p += p.v * Particle::DT }
|
497
|
+
|
498
|
+
# check system energy delta or iteration count
|
499
|
+
|
500
|
+
es << g.energy
|
501
|
+
|
502
|
+
if es.length >= CW_SZ
|
503
|
+
es = es[-CW_SZ,CW_SZ]
|
504
|
+
pses = ses
|
505
|
+
ses = es[-CW_SZ,CW_SZ].inject(0.0) { |sum,e| sum += e }.to_f / CW_SZ
|
506
|
+
de = ses - pses
|
507
|
+
break if de.abs < 0.005
|
508
|
+
else
|
509
|
+
ses = 1000
|
510
|
+
de = 1000
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
self
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
end # module ChatChart
|
data/lib/particle.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
#
|
2
|
+
# particle.rb - Physics Particle Code
|
3
|
+
#
|
4
|
+
# Copyright(c) 2009 by Christopher Abad
|
5
|
+
# aempirei@gmail.com
|
6
|
+
# http://www.twentygoto10.com/
|
7
|
+
#
|
8
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
9
|
+
# if i don't like how you use this software i can tell you to fuck off
|
10
|
+
# and you can't use it, otherwise you can use it.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'point'
|
14
|
+
|
15
|
+
module ChatChart
|
16
|
+
class Particle
|
17
|
+
|
18
|
+
DT = 0.50 # time delta
|
19
|
+
G = -20.0 # universal constant of gravity
|
20
|
+
M = 300.0 # electron charge bond constant
|
21
|
+
Z = 0.90 # friction
|
22
|
+
D = 8.00 # bond distance
|
23
|
+
|
24
|
+
EPSILON = 0.03
|
25
|
+
NORMMIN = 0.002
|
26
|
+
|
27
|
+
attr_accessor :p # position
|
28
|
+
attr_accessor :m # mass
|
29
|
+
attr_accessor :v # velocity
|
30
|
+
attr_accessor :a # acceleration
|
31
|
+
attr_accessor :b # bond
|
32
|
+
|
33
|
+
def hash
|
34
|
+
object_id.hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.[](*ns)
|
38
|
+
self.new P[*ns].vector
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(p=P[0.0,0.0])
|
42
|
+
@m = 1.0
|
43
|
+
@v = P[0.0,0.0]
|
44
|
+
@p = p
|
45
|
+
end
|
46
|
+
|
47
|
+
def acceleration(p)
|
48
|
+
force(p) / @m
|
49
|
+
end
|
50
|
+
|
51
|
+
# electron bond attraction
|
52
|
+
def bond(p)
|
53
|
+
v = p.p - self.p
|
54
|
+
return P.zero if v == P.zero
|
55
|
+
return P.zero if v.aspect.norm < D - EPSILON
|
56
|
+
v0 = v.aspect.norm + D
|
57
|
+
v.unit * M / (v0 * v0)
|
58
|
+
end
|
59
|
+
|
60
|
+
def n_body_acceleration(ps)
|
61
|
+
v = ps.inject(P[].vector) { |acc,p| acc += acceleration p }
|
62
|
+
v.aspect.norm < NORMMIN ? P.zero : v
|
63
|
+
end
|
64
|
+
|
65
|
+
def n_body_bond(ps)
|
66
|
+
v = ps.inject(P[].vector) { |acc,p| acc += bond p }
|
67
|
+
v.aspect.norm < NORMMIN ? P.zero : v
|
68
|
+
end
|
69
|
+
|
70
|
+
# monopole short acting repulsive nuclear force
|
71
|
+
def force(p)
|
72
|
+
v = p.p - self.p
|
73
|
+
return P.zero if v == P.zero
|
74
|
+
return P.zero if v.aspect.norm > D + EPSILON
|
75
|
+
v0 = v.aspect.norm + D
|
76
|
+
v.unit * G * @m * p.m / (v0 * v0)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class GParticle < Particle
|
81
|
+
DT = 0.5 # time delta
|
82
|
+
G = 50.0 # universal constant of gravity
|
83
|
+
Z = 0.98 # friction
|
84
|
+
D = 3.00 # bond distance
|
85
|
+
# monopole short acting repulsive nuclear force
|
86
|
+
def force(p)
|
87
|
+
v = p.p - self.p
|
88
|
+
return P.zero if v == P.zero
|
89
|
+
v0 = v.aspect.norm + D
|
90
|
+
v.unit * G * @m * p.m / (v0 * v0)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/point.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
#
|
2
|
+
# Point
|
3
|
+
# point.rb - Simple 2-D Point
|
4
|
+
#
|
5
|
+
# Copyright(c) 2009 by Christopher Abad
|
6
|
+
# aempirei@gmail.com
|
7
|
+
# http://www.twentygoto10.com/
|
8
|
+
#
|
9
|
+
# this code is licensed under the "don't be a retarded asshole" license.
|
10
|
+
# if i don't like how you use this software i can tell you to fuck off
|
11
|
+
# and you can't use it, otherwise you can use it.
|
12
|
+
#
|
13
|
+
|
14
|
+
module ChatChart
|
15
|
+
class P
|
16
|
+
attr_accessor :x
|
17
|
+
attr_accessor :y
|
18
|
+
|
19
|
+
def initialize(*ns)
|
20
|
+
@x = ns[-2] || 0
|
21
|
+
@y = ns[-1] || 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.[](*ns)
|
25
|
+
new *ns
|
26
|
+
end
|
27
|
+
|
28
|
+
I = P[1,0]
|
29
|
+
J = P[0,1]
|
30
|
+
|
31
|
+
def self.zero
|
32
|
+
return P[]
|
33
|
+
end
|
34
|
+
|
35
|
+
def randomize!(width,height)
|
36
|
+
@x = rand width
|
37
|
+
@y = rand height
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.random(width,height)
|
42
|
+
new.randomize! width, height
|
43
|
+
end
|
44
|
+
|
45
|
+
def quantize!
|
46
|
+
self << self.quantize
|
47
|
+
end
|
48
|
+
|
49
|
+
def quantize
|
50
|
+
P[@x.round.to_i, @y.round.to_i]
|
51
|
+
end
|
52
|
+
|
53
|
+
def vector
|
54
|
+
P[@x.to_f,@y.to_f]
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
"(%05.1f,%05.1f)" % [@x,@y]
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_a
|
62
|
+
[@x,@y]
|
63
|
+
end
|
64
|
+
|
65
|
+
def eql?(p)
|
66
|
+
p.is_a?(P) and (@x == p.x) and (@y == p.y)
|
67
|
+
end
|
68
|
+
|
69
|
+
def hash
|
70
|
+
[@x,@y].hash
|
71
|
+
end
|
72
|
+
|
73
|
+
def <=>(p)
|
74
|
+
(@x * @x + @y * @y) <=> (p.x * p.x + p.y * p.y)
|
75
|
+
end
|
76
|
+
|
77
|
+
# vector addition
|
78
|
+
def +(p)
|
79
|
+
P[@x + p.x, @y + p.y]
|
80
|
+
end
|
81
|
+
|
82
|
+
# both scalar product and dot product
|
83
|
+
def *(v)
|
84
|
+
case v
|
85
|
+
when P
|
86
|
+
(@x * v.x + @y * v.y)
|
87
|
+
else
|
88
|
+
P[@x * v, @y * v]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def /(v)
|
93
|
+
self * (1.0 / v.to_f)
|
94
|
+
end
|
95
|
+
|
96
|
+
def unit
|
97
|
+
return P.zero if self == P.zero
|
98
|
+
self / norm
|
99
|
+
end
|
100
|
+
|
101
|
+
def comp(b)
|
102
|
+
norm * cos(b)
|
103
|
+
end
|
104
|
+
|
105
|
+
def monic
|
106
|
+
self.vector / @x
|
107
|
+
end
|
108
|
+
|
109
|
+
def proj(b)
|
110
|
+
b.unit * (self * b.unit)
|
111
|
+
end
|
112
|
+
|
113
|
+
def cos(b)
|
114
|
+
unit * b.unit
|
115
|
+
end
|
116
|
+
|
117
|
+
S2 = Math::sqrt(2.0)
|
118
|
+
|
119
|
+
def aspect(k=1.3)
|
120
|
+
P[@x, @y * k]
|
121
|
+
end
|
122
|
+
|
123
|
+
def norm(v=2)
|
124
|
+
case v
|
125
|
+
when 1
|
126
|
+
(@x.abs + @y.abs).to_f
|
127
|
+
when 2
|
128
|
+
Math::hypot @x, @y
|
129
|
+
when Numeric
|
130
|
+
(@x.abs ** v + @y.abs ** v) ** (1.0 / v)
|
131
|
+
when :ascii
|
132
|
+
aspect.norm
|
133
|
+
when :min
|
134
|
+
[@x.abs,@y.abs].min
|
135
|
+
when :max
|
136
|
+
[@x.abs,@y.abs].max
|
137
|
+
when :h
|
138
|
+
norm(:min) * (S2 - 1) + norm(:max)
|
139
|
+
else
|
140
|
+
1.0
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def <<(p)
|
145
|
+
@x = p.x
|
146
|
+
@y = p.y
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def u! ; self << self.u ; end
|
151
|
+
def d! ; self << self.d ; end
|
152
|
+
def l! ; self << self.l ; end
|
153
|
+
def r! ; self << self.r ; end
|
154
|
+
|
155
|
+
def u ; P[@x,@y-1] ; end
|
156
|
+
def d ; P[@x,@y+1] ; end
|
157
|
+
def l ; P[@x-1,@y] ; end
|
158
|
+
def r ; P[@x+1,@y] ; end
|
159
|
+
|
160
|
+
def ==(p) ; eql?(p) ; end
|
161
|
+
def %(p) ; P[@x,p.y] ; end
|
162
|
+
def -@ ; P[-@x,-@y] ; end
|
163
|
+
def +@ ; self ; end
|
164
|
+
def ~@ ; P[@y,@x] ; end
|
165
|
+
def -(p) ; self + -p ; end
|
166
|
+
end
|
167
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chatchart
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christopher Abad
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-22 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: This is a package of various crappy ruby code which generally revolves around ASCII drawing and graph layout.
|
17
|
+
email: aempirei@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- demos/demo-animated.rb
|
26
|
+
- demos/demo-simple.rb
|
27
|
+
- demos/demo-physics.rb
|
28
|
+
- demos/demo-automatic.rb
|
29
|
+
- lib/chatchart.rb
|
30
|
+
- lib/particle.rb
|
31
|
+
- lib/point.rb
|
32
|
+
- README
|
33
|
+
- LICENSE
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://www.twentygoto10.com/
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.5
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: ASCII drawing and graph layout
|
62
|
+
test_files: []
|
63
|
+
|