ccharts 3.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.
- checksums.yaml +7 -0
- data/ext/ccharts/extconf.rb +20 -0
- data/ext/ccharts/vendor/ccharts.h +3088 -0
- data/ext/ccharts/vendor/ccharts_abi.c +620 -0
- data/ext/ccharts/vendor/ccharts_abi.h +483 -0
- data/lib/ccharts/chart.rb +253 -0
- data/lib/ccharts/color.rb +46 -0
- data/lib/ccharts/ffi.rb +217 -0
- data/lib/ccharts/settings.rb +365 -0
- data/lib/ccharts/version.rb +7 -0
- data/lib/ccharts.rb +50 -0
- metadata +73 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ccharts
|
|
4
|
+
# High-level chart API. A dataset built with {Chart.from_arrays},
|
|
5
|
+
# {Chart.from_json} or {Chart.from_csv} is immutable; the pie / histogram /
|
|
6
|
+
# sparkline / bar / stacked_bar / heatmap / boxplot renderers take their data
|
|
7
|
+
# directly (they have no OHLC dataset, exactly like the C ABI they wrap).
|
|
8
|
+
class Chart
|
|
9
|
+
# ------------------------------------------------------------------
|
|
10
|
+
# Building a dataset
|
|
11
|
+
# ------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
# Build a dataset from four equal-length price columns and optional epoch
|
|
14
|
+
# seconds. `ts` may be nil (all timestamps unknown).
|
|
15
|
+
def self.from_arrays(open:, high:, low:, close:, ts: nil)
|
|
16
|
+
n = open.length
|
|
17
|
+
raise Error, "need at least one candle" if n == 0
|
|
18
|
+
unless [high, low, close].all? { |a| a.length == n }
|
|
19
|
+
raise Error, "open, high, low and close must have the same length"
|
|
20
|
+
end
|
|
21
|
+
if ts && ts.length != n
|
|
22
|
+
raise Error, "ts must have the same length as the price columns"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
open_ptr = FFI.pack_doubles(open)
|
|
26
|
+
high_ptr = FFI.pack_doubles(high)
|
|
27
|
+
low_ptr = FFI.pack_doubles(low)
|
|
28
|
+
close_ptr = FFI.pack_doubles(close)
|
|
29
|
+
ts_ptr = ts ? FFI.pack_i64(ts) : Fiddle::NULL
|
|
30
|
+
|
|
31
|
+
out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
32
|
+
status = FFI::FROM_ARRAYS.call(open_ptr, high_ptr, low_ptr, close_ptr, ts_ptr, n, out)
|
|
33
|
+
_from_data_status(status, out)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build a dataset from the fixed-schema JSON document accepted by the
|
|
37
|
+
# C layer (an array of {ts, open, high, low, close} objects).
|
|
38
|
+
def self.from_json(text)
|
|
39
|
+
json = FFI.cstr(text)
|
|
40
|
+
out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
41
|
+
status = FFI::PARSE_JSON.call(json, out)
|
|
42
|
+
_from_data_status(status, out)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build a dataset from CSV rows of `open,high,low,close[,timestamp]`.
|
|
46
|
+
def self.from_csv(text, value_separator: ",", line_separator: "\n")
|
|
47
|
+
csv = FFI.cstr(text)
|
|
48
|
+
vs = value_separator.ord
|
|
49
|
+
ls = line_separator.ord
|
|
50
|
+
raise Error, "separators must not be NUL" if vs == 0 || ls == 0
|
|
51
|
+
out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
52
|
+
status = FFI::PARSE_CSV.call(csv, vs, ls, out)
|
|
53
|
+
_from_data_status(status, out)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self._from_data_status(status, out)
|
|
57
|
+
Ccharts.raise_on(status)
|
|
58
|
+
addr = FFI.read_ptr_int(out, Fiddle::SIZEOF_VOIDP)
|
|
59
|
+
raise Error, "no dataset handle returned" if addr.zero?
|
|
60
|
+
_from_handle(Fiddle::Pointer.new(addr))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# Rendering an OHLC dataset (line / candle)
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def line(width, height, settings = nil)
|
|
68
|
+
_render(FFI::LINE, width, height, settings || Settings::ChartSettings.new)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def candle(width, height, settings = nil)
|
|
72
|
+
_render(FFI::CANDLE, width, height, settings || Settings::ChartSettings.new)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Number of candles in this dataset.
|
|
76
|
+
def size
|
|
77
|
+
FFI::DATA_LEN.call(@handle)
|
|
78
|
+
end
|
|
79
|
+
alias_method :length, :size
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Pie / histogram / sparkline / bar / stack / heat / box renderers
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def self.pie(slices, width, height, settings = nil)
|
|
86
|
+
raise Error, "need at least one slice" if slices.empty?
|
|
87
|
+
settings ||= Settings::PieSettings.new
|
|
88
|
+
|
|
89
|
+
keep = []
|
|
90
|
+
rows = slices.map do |sl|
|
|
91
|
+
label = sl[:label] || sl["label"]
|
|
92
|
+
value = (sl[:value] || sl["value"] || 0.0).to_f
|
|
93
|
+
lp = FFI.cstr(label)
|
|
94
|
+
keep << lp
|
|
95
|
+
[[0, 8, "Q", lp.to_i], [8, 8, "d", value]] # label @0, value @8
|
|
96
|
+
end
|
|
97
|
+
slices_ptr = FFI.contiguous_structs(16, rows) # ccharts_pie_slice = 16 B
|
|
98
|
+
keep << slices_ptr
|
|
99
|
+
|
|
100
|
+
colors_ptr_addr = settings.colors_ptr(keep).to_i
|
|
101
|
+
center_ptr = settings.center_text_ptr(keep)
|
|
102
|
+
donut, show_legend, show_pct, slice_gap, inner_radius_ratio,
|
|
103
|
+
legend_format, start_angle, counter_clockwise = settings._scalars
|
|
104
|
+
|
|
105
|
+
# `_scalars` already returns the flags as C ints 0/1 — pass them straight
|
|
106
|
+
# through. Do NOT write `x ? 1 : 0` here: in Ruby `0` is truthy, so a
|
|
107
|
+
# zeroed flag would be coerced to 1 (pct/ccw always on → mirrored pie).
|
|
108
|
+
_read_string(
|
|
109
|
+
FFI::PIE, slices_ptr, slices.length, width, height,
|
|
110
|
+
donut, colors_ptr_addr, settings.colors_count,
|
|
111
|
+
show_legend, show_pct,
|
|
112
|
+
slice_gap, inner_radius_ratio, legend_format,
|
|
113
|
+
start_angle, counter_clockwise, center_ptr
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.histogram(samples, width, height, settings = nil)
|
|
118
|
+
raise Error, "need at least one sample" if samples.empty?
|
|
119
|
+
settings ||= Settings::HistSettings.new
|
|
120
|
+
ptr = FFI.pack_doubles(samples)
|
|
121
|
+
s, keep = settings.to_ffi
|
|
122
|
+
_read_string(FFI::HIST, ptr, samples.length, width, height, s.to_ptr)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.sparkline(samples, width, height, settings = nil)
|
|
126
|
+
raise Error, "need at least one sample" if samples.empty?
|
|
127
|
+
settings ||= Settings::SparkSettings.new
|
|
128
|
+
ptr = FFI.pack_doubles(samples)
|
|
129
|
+
s, keep = settings.to_ffi
|
|
130
|
+
_read_string(FFI::SPARK, ptr, samples.length, width, height, s.to_ptr)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.bar(labels, values, width, height, settings = nil)
|
|
134
|
+
raise Error, "need at least one bar" if labels.empty?
|
|
135
|
+
raise Error, "labels and values must have the same length" unless labels.length == values.length
|
|
136
|
+
settings ||= Settings::BarSettings.new
|
|
137
|
+
|
|
138
|
+
keep = []
|
|
139
|
+
rows = labels.zip(values).map do |lbl, val|
|
|
140
|
+
lp = FFI.cstr(lbl)
|
|
141
|
+
keep << lp
|
|
142
|
+
[[0, 8, "Q", lp.to_i], [8, 8, "d", val.to_f]] # label @0, value @8
|
|
143
|
+
end
|
|
144
|
+
items_ptr = FFI.contiguous_structs(16, rows) # ccharts_bar_slice = 16 B
|
|
145
|
+
keep << items_ptr
|
|
146
|
+
s, skeep = settings.to_ffi
|
|
147
|
+
keep.concat(skeep)
|
|
148
|
+
_read_string(FFI::BAR, items_ptr, labels.length, width, height, s.to_ptr)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# series: array of {name:, values:} hashes sharing one category count.
|
|
152
|
+
def self.stacked_bar(series, width, height, settings = nil)
|
|
153
|
+
raise Error, "need at least one series" if series.empty?
|
|
154
|
+
cats = series[0][:values]&.length || series[0]["values"].length
|
|
155
|
+
raise Error, "series values must not be empty" if cats == 0
|
|
156
|
+
series.each do |sv|
|
|
157
|
+
vs = sv[:values] || sv["values"]
|
|
158
|
+
raise Error, "all series must have the same number of values" unless vs.length == cats
|
|
159
|
+
end
|
|
160
|
+
settings ||= Settings::StackSettings.new
|
|
161
|
+
settings.counts(series.length, cats)
|
|
162
|
+
|
|
163
|
+
keep = []
|
|
164
|
+
rows = series.map do |sv|
|
|
165
|
+
vs = sv[:values] || sv["values"]
|
|
166
|
+
name = sv[:name] || sv["name"]
|
|
167
|
+
np = FFI.cstr(name)
|
|
168
|
+
vp = FFI.pack_doubles(vs)
|
|
169
|
+
keep << np << vp
|
|
170
|
+
[[0, 8, "Q", np.to_i], [8, 8, "Q", vp.to_i]] # name @0, values @8
|
|
171
|
+
end
|
|
172
|
+
series_ptr = FFI.contiguous_structs(16, rows) # ccharts_stack_series = 16 B
|
|
173
|
+
keep << series_ptr
|
|
174
|
+
s, skeep = settings.to_ffi
|
|
175
|
+
keep.concat(skeep)
|
|
176
|
+
_read_string(FFI::STACK, series_ptr, series.length, width, height, s.to_ptr)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# values: a rows x cols matrix (array of equal-length arrays).
|
|
180
|
+
def self.heatmap(values, width, height, settings = nil)
|
|
181
|
+
raise Error, "need a non-empty value matrix" if values.empty?
|
|
182
|
+
cols = values[0].length
|
|
183
|
+
raise Error, "matrix columns must not be empty" if cols == 0
|
|
184
|
+
values.each { |row| raise Error, "all rows must have the same number of values" unless row.length == cols }
|
|
185
|
+
settings ||= Settings::HeatSettings.new
|
|
186
|
+
|
|
187
|
+
flat = values.flatten
|
|
188
|
+
flat_ptr = FFI.pack_doubles(flat)
|
|
189
|
+
s, keep = settings.to_ffi
|
|
190
|
+
_read_string(FFI::HEAT, flat_ptr, values.length, cols, width, height, s.to_ptr)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# series: array of {name:, samples:} hashes (per-category samples).
|
|
194
|
+
def self.boxplot(series, width, height, settings = nil)
|
|
195
|
+
raise Error, "need at least one category" if series.empty?
|
|
196
|
+
series.each do |c|
|
|
197
|
+
sm = c[:samples] || c["samples"]
|
|
198
|
+
raise Error, "every category must have at least one sample" if sm.empty?
|
|
199
|
+
end
|
|
200
|
+
settings ||= Settings::BoxSettings.new
|
|
201
|
+
|
|
202
|
+
keep = []
|
|
203
|
+
rows = series.map do |c|
|
|
204
|
+
sm = c[:samples] || c["samples"]
|
|
205
|
+
name = c[:name] || c["name"]
|
|
206
|
+
np = FFI.cstr(name)
|
|
207
|
+
sp = FFI.pack_doubles(sm)
|
|
208
|
+
keep << np << sp
|
|
209
|
+
[[0, 8, "Q", np.to_i], [8, 8, "Q", sp.to_i], [16, 4, "l", sm.length]] # name@0, samples@8, n@16 (24 B)
|
|
210
|
+
end
|
|
211
|
+
cats_ptr = FFI.contiguous_structs(24, rows) # ccharts_box_category = 24 B
|
|
212
|
+
keep << cats_ptr
|
|
213
|
+
s, skeep = settings.to_ffi
|
|
214
|
+
keep.concat(skeep)
|
|
215
|
+
_read_string(FFI::BOX, cats_ptr, series.length, width, height, s.to_ptr)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Internals
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def self._from_handle(handle)
|
|
223
|
+
obj = allocate
|
|
224
|
+
obj.instance_variable_set(:@handle, handle)
|
|
225
|
+
ObjectSpace.define_finalizer(obj, _finalizer(handle.to_i))
|
|
226
|
+
obj
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def self._finalizer(addr)
|
|
230
|
+
proc { FFI::DATA_FREE.call(Fiddle::Pointer.new(addr)) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def _render(fn, width, height, settings)
|
|
234
|
+
s, = settings.to_ffi
|
|
235
|
+
self.class._read_string(fn, @handle, width, height, s.to_ptr)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Call a render function and copy the returned C string, releasing it.
|
|
239
|
+
def self._read_string(fn, *args)
|
|
240
|
+
out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
241
|
+
out_len = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SIZE_T)
|
|
242
|
+
status = fn.call(*args, out, out_len)
|
|
243
|
+
Ccharts.raise_on(status)
|
|
244
|
+
addr = FFI.read_ptr_int(out, Fiddle::SIZEOF_VOIDP)
|
|
245
|
+
len = FFI.read_ptr_int(out_len, Fiddle::SIZEOF_SIZE_T)
|
|
246
|
+
s = Fiddle::Pointer.new(addr).to_s(len).force_encoding("UTF-8")
|
|
247
|
+
FFI::STRING_FREE.call(Fiddle::Pointer.new(addr))
|
|
248
|
+
s
|
|
249
|
+
end
|
|
250
|
+
private_class_method :_from_handle, :_from_data_status, :_finalizer
|
|
251
|
+
private :_render
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ccharts
|
|
4
|
+
# ANSI color escapes matching the C library's ccharts_color_index values
|
|
5
|
+
# (abi/ccharts_abi.h). These come straight from the library: a binding passes
|
|
6
|
+
# the escape string (or NULL for the library default) through the settings
|
|
7
|
+
# structs, so every binding resolves the same names to the same escapes.
|
|
8
|
+
module Color
|
|
9
|
+
ESCAPES = {
|
|
10
|
+
"black" => "\e[30m",
|
|
11
|
+
"red" => "\e[31m",
|
|
12
|
+
"green" => "\e[32m",
|
|
13
|
+
"yellow" => "\e[33m",
|
|
14
|
+
"blue" => "\e[34m",
|
|
15
|
+
"magenta" => "\e[35m",
|
|
16
|
+
"cyan" => "\e[36m",
|
|
17
|
+
"white" => "\e[37m",
|
|
18
|
+
"bright_black" => "\e[90m",
|
|
19
|
+
"bright_red" => "\e[91m",
|
|
20
|
+
"bright_green" => "\e[92m",
|
|
21
|
+
"bright_yellow"=> "\e[93m",
|
|
22
|
+
"bright_blue" => "\e[94m",
|
|
23
|
+
"bright_magenta"=> "\e[95m",
|
|
24
|
+
"bright_cyan" => "\e[96m",
|
|
25
|
+
"bright_white" => "\e[97m",
|
|
26
|
+
"reset" => "\e[0m",
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Resolve a color given either a name ("blue"), a Symbol (:blue), or a raw
|
|
30
|
+
# ANSI escape sequence (256-color/truecolor strings passed straight through).
|
|
31
|
+
# Returns the escape string, or nil if the value is nil/empty (library
|
|
32
|
+
# default). Unknown names fall back to the literal string so raw escapes
|
|
33
|
+
# keep working.
|
|
34
|
+
def self.resolve(value)
|
|
35
|
+
return nil if value.nil?
|
|
36
|
+
s = value.to_s
|
|
37
|
+
return nil if s.empty?
|
|
38
|
+
ESCAPES[s] || s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The ANSI escape for a named color, or nil when unknown.
|
|
42
|
+
def self.[](name)
|
|
43
|
+
ESCAPES[name.to_s]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/ccharts/ffi.rb
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "fiddle/import"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
|
|
7
|
+
module Ccharts
|
|
8
|
+
# Raw Fiddle bindings to the vendored ccharts ABI (ext/ccharts/vendor).
|
|
9
|
+
#
|
|
10
|
+
# The extension (ccharts_ext.so) is built from ext/ccharts/vendor/ccharts_abi.c
|
|
11
|
+
# by mkmf — it is pure object code, so Fiddle just dlopens it and resolves
|
|
12
|
+
# the 20 ccharts_* symbols below. The struct layouts mirror abi/ccharts_abi.h
|
|
13
|
+
# exactly (field order + C alignment); a mismatch shows up as a conformance
|
|
14
|
+
# failure, not a crash.
|
|
15
|
+
module FFI
|
|
16
|
+
extend Fiddle::Importer
|
|
17
|
+
|
|
18
|
+
DLEXT = RbConfig::CONFIG["DLEXT"] # e.g. "so"
|
|
19
|
+
|
|
20
|
+
# ---- native library location ---------------------------------------
|
|
21
|
+
def self.native_candidates
|
|
22
|
+
lib_dir = File.expand_path("..", __dir__) # .../lib
|
|
23
|
+
here = __dir__ # .../lib/ccharts
|
|
24
|
+
repo_ext = File.expand_path("../../ext/ccharts", here) # repo build dir
|
|
25
|
+
[
|
|
26
|
+
File.join(lib_dir, "ccharts_ext.#{DLEXT}"), # dev build drops it in lib/
|
|
27
|
+
File.join(here, "ccharts_ext.#{DLEXT}"), # some install layouts
|
|
28
|
+
File.join(repo_ext, "ccharts_ext.#{DLEXT}"), # built in-place
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
LIB = begin
|
|
33
|
+
path = native_candidates.find { |p| File.file?(p) }
|
|
34
|
+
raise LoadError, "ccharts native (ccharts_ext.#{DLEXT}) not built — run `rake compile` (or the Rakefile :compile task) first" unless path
|
|
35
|
+
Fiddle.dlopen(path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ---- type constants -------------------------------------------------
|
|
39
|
+
T_VOID = Fiddle::TYPE_VOID
|
|
40
|
+
T_VOIDP = Fiddle::TYPE_VOIDP
|
|
41
|
+
T_INT = Fiddle::TYPE_INT
|
|
42
|
+
T_DOUBLE = Fiddle::TYPE_DOUBLE
|
|
43
|
+
T_CHAR = Fiddle::TYPE_CHAR
|
|
44
|
+
T_LL = Fiddle::TYPE_LONG_LONG
|
|
45
|
+
|
|
46
|
+
def self.fn(name, args, ret)
|
|
47
|
+
Fiddle::Function.new(LIB[name], args, ret)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ---- the 20 exported ccharts_* functions ----------------------------
|
|
51
|
+
FROM_ARRAYS = fn("ccharts_from_arrays", [T_VOIDP, T_VOIDP, T_VOIDP, T_VOIDP, T_VOIDP, T_INT, T_VOIDP], T_INT)
|
|
52
|
+
PARSE_JSON = fn("ccharts_parse_json", [T_VOIDP, T_VOIDP], T_INT)
|
|
53
|
+
PARSE_CSV = fn("ccharts_parse_csv", [T_VOIDP, T_CHAR, T_CHAR, T_VOIDP], T_INT)
|
|
54
|
+
DATA_LEN = fn("ccharts_data_len", [T_VOIDP], T_INT)
|
|
55
|
+
DATA_FREE = fn("ccharts_data_free", [T_VOIDP], T_VOID)
|
|
56
|
+
STRING_FREE = fn("ccharts_string_free", [T_VOIDP], T_VOID)
|
|
57
|
+
LINE = fn("ccharts_line", [T_VOIDP, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
58
|
+
CANDLE = fn("ccharts_candle", [T_VOIDP, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
59
|
+
PIE = fn("ccharts_pie_from_slices",
|
|
60
|
+
[T_VOIDP, T_INT, T_INT, T_INT, T_INT, T_VOIDP, T_INT, T_INT, T_INT,
|
|
61
|
+
T_DOUBLE, T_DOUBLE, T_INT, T_DOUBLE, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
62
|
+
HIST = fn("ccharts_hist", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
63
|
+
SPARK = fn("ccharts_spark", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
64
|
+
BAR = fn("ccharts_bar", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
65
|
+
STACK = fn("ccharts_stack", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
66
|
+
HEAT = fn("ccharts_heat", [T_VOIDP, T_INT, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
67
|
+
BOX = fn("ccharts_box", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
|
|
68
|
+
COLOR = fn("ccharts_color", [T_INT], T_VOIDP)
|
|
69
|
+
ERROR_MSG = fn("ccharts_error_message", [T_INT], T_VOIDP)
|
|
70
|
+
VERSION = fn("ccharts_version", [], T_VOIDP)
|
|
71
|
+
MAX_DIM = fn("ccharts_max_dim", [], T_INT)
|
|
72
|
+
MAX_CELLS = fn("ccharts_max_cells", [], T_INT)
|
|
73
|
+
|
|
74
|
+
# ---- settings structs (layouts from abi/ccharts_abi.h) --------------
|
|
75
|
+
# Every color is a pointer (void*); ints are int32. The Fiddle importer
|
|
76
|
+
# applies the same alignment/padding rules as the C compiler, so these
|
|
77
|
+
# match the ABI byte-for-byte.
|
|
78
|
+
Settings = struct [
|
|
79
|
+
"void* rise_color",
|
|
80
|
+
"void* fall_color",
|
|
81
|
+
"void* bg_color",
|
|
82
|
+
"void* area_color",
|
|
83
|
+
"int single_color",
|
|
84
|
+
"int show_prices",
|
|
85
|
+
"int show_times",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
PieSlice = struct [
|
|
89
|
+
"void* label",
|
|
90
|
+
"double value",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
HistSettings = struct [
|
|
94
|
+
"void* rise_color",
|
|
95
|
+
"void* bg_color",
|
|
96
|
+
"int bin_count",
|
|
97
|
+
"double min_value",
|
|
98
|
+
"double max_value",
|
|
99
|
+
"int show_bins",
|
|
100
|
+
"int show_prices",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
SparkSettings = struct [
|
|
104
|
+
"void* rise_color",
|
|
105
|
+
"void* area_color",
|
|
106
|
+
"int min_above",
|
|
107
|
+
"int min_below",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
BarSlice = struct [
|
|
111
|
+
"void* label",
|
|
112
|
+
"double value",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
BarSettings = struct [
|
|
116
|
+
"void* rise_color",
|
|
117
|
+
"void* bg_color",
|
|
118
|
+
"int show_labels",
|
|
119
|
+
"int show_prices",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
StackSeries = struct [
|
|
123
|
+
"void* name",
|
|
124
|
+
"void* values",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
StackSettings = struct [
|
|
128
|
+
"void* colors",
|
|
129
|
+
"void* bg_color",
|
|
130
|
+
"void* cat_labels",
|
|
131
|
+
"int series",
|
|
132
|
+
"int cats",
|
|
133
|
+
"int show_labels",
|
|
134
|
+
"int show_prices",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
HeatSettings = struct [
|
|
138
|
+
"void* low_color",
|
|
139
|
+
"void* high_color",
|
|
140
|
+
"void* mid_color",
|
|
141
|
+
"void* bg_color",
|
|
142
|
+
"void* row_labels",
|
|
143
|
+
"void* col_labels",
|
|
144
|
+
"int show_labels",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
BoxCategory = struct [
|
|
148
|
+
"void* name",
|
|
149
|
+
"void* samples",
|
|
150
|
+
"int n",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
BoxSettings = struct [
|
|
154
|
+
"void* rise_color",
|
|
155
|
+
"void* area_color",
|
|
156
|
+
"void* bg_color",
|
|
157
|
+
"int show_prices",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# ---- helpers ---------------------------------------------------------
|
|
161
|
+
# A never-referenced empty string (plain mode: no ANSI escape at all).
|
|
162
|
+
EMPTY = Fiddle::Pointer["\0"]
|
|
163
|
+
|
|
164
|
+
# Turn a color value into a NUL-terminated escape pointer. `plain` forces
|
|
165
|
+
# the empty escape (overriding even a caller color) so no ANSI bytes are
|
|
166
|
+
# emitted. nil -> 0 (library default); else the resolved escape string.
|
|
167
|
+
def self.color_ptr(value, plain)
|
|
168
|
+
return EMPTY if plain
|
|
169
|
+
esc = Color.resolve(value)
|
|
170
|
+
return Fiddle::NULL if esc.nil?
|
|
171
|
+
Fiddle::Pointer[esc.b + "\0"]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Pack a Ruby array of doubles into a C double[] pointer.
|
|
175
|
+
def self.pack_doubles(arr)
|
|
176
|
+
Fiddle::Pointer[arr.pack("d*")]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Build ONE contiguous C array of `count` structs, each `size` bytes, so a
|
|
180
|
+
# `const ccharts_foo*`/array-of-structs argument points at a real C-layout
|
|
181
|
+
# block (`&arr[i]` is at `base + i*size`). Do NOT pass an array of pointers
|
|
182
|
+
# to separately-malloc'd structs — C reads this as a contiguous block and a
|
|
183
|
+
# pointer-array makes it read garbage at offset `i*size` (segfault / wrong
|
|
184
|
+
# data). `rows` is one entry per element: an array of `[offset, nbytes,
|
|
185
|
+
# pack_fmt, value]` field setters relative to that element's base.
|
|
186
|
+
def self.contiguous_structs(size, rows)
|
|
187
|
+
block = Fiddle::Pointer.malloc(size * rows.length)
|
|
188
|
+
rows.each_with_index do |fields, i|
|
|
189
|
+
base = i * size
|
|
190
|
+
fields.each { |off, nbytes, fmt, val| block[base + off, nbytes] = [val].pack(fmt) }
|
|
191
|
+
end
|
|
192
|
+
block
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Pack a Ruby array of integers into a C int64_t[] pointer.
|
|
196
|
+
def self.pack_i64(arr)
|
|
197
|
+
Fiddle::Pointer[arr.pack("q*")]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# NUL-terminated pointer for a Ruby string (or Fiddle::NULL for nil).
|
|
201
|
+
def self.cstr(str)
|
|
202
|
+
return Fiddle::NULL if str.nil?
|
|
203
|
+
Fiddle::Pointer[str.b + "\0"]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Read a C string (NUL-terminated) from a returned pointer.
|
|
207
|
+
def self.read_cstr(ptr)
|
|
208
|
+
return nil if ptr.nil? || ptr.to_i == 0
|
|
209
|
+
ptr.to_s
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Read the integer value pointed to by a Fiddle::Pointer of the given size.
|
|
213
|
+
def self.read_ptr_int(buf, size)
|
|
214
|
+
buf[0, size].unpack1(size == 8 ? "Q" : "L")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|