Skip to content

Commit 1c98e7c

Browse files
committed
Implement quantity-aware plotting
1 parent f6d38b9 commit 1c98e7c

5 files changed

Lines changed: 394 additions & 26 deletions

File tree

spec/reference_spec.cr

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,12 @@ describe "Julia reference output compatibility" do
319319

320320
it "matches lineplot/units_pos_vel" do
321321
t = (0..100).map(&.to_f64)
322-
pos = t.map { |val| 0.5 * val * val }
323-
vel = t
322+
pos_values = t.map { |val| 0.5 * val * val }
323+
vel_values = t
324+
pos = UnicodePlot.quantity(pos_values, "m")
325+
vel = UnicodePlot.quantity(vel_values, "m s⁻¹")
324326
p = UnicodePlot.lineplot(pos, vel, xlabel: "position (m)", ylabel: "speed (m s⁻¹)")
325-
UnicodePlot.lineplot!(p, [pos.min, pos.max], [vel.max, vel.max], color: :red)
327+
UnicodePlot.lineplot!(p, [pos_values.min, pos_values.max], [vel_values.max, vel_values.max], color: :red)
326328
test_ref("lineplot/units_pos_vel.txt", p)
327329
end
328330

@@ -516,12 +518,10 @@ describe "Julia reference output compatibility" do
516518
end
517519

518520
it "matches scatterplot/units_temp" do
519-
p = UnicodePlot.scatterplot(
520-
[22.0, 23.0, 24.0],
521-
marker: :circle,
522-
ylabel: "°C",
523-
)
524-
UnicodePlot.scatterplot!(p, [23.5, 22.5, 23.0], marker: :cross, color: :red)
521+
y1 = UnicodePlot.quantity([22.0, 23.0, 24.0], "°C")
522+
p = UnicodePlot.scatterplot(y1, marker: :circle)
523+
y2 = UnicodePlot.quantity([23.5, 22.5, 23.0], "°C")
524+
UnicodePlot.scatterplot!(p, y2, marker: :cross, color: :red)
525525
test_ref("scatterplot/units_temp.txt", p)
526526
end
527527
end

spec/unicode_plot_spec.cr

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ describe UnicodePlot do
129129
p = UnicodePlot.lineplot([Float64::MAX, 0.5], [0.5, 0.5])
130130
p.to_s.should be_a(String)
131131
end
132+
133+
it "auto-applies unit labels from quantity arrays" do
134+
x = UnicodePlot.quantity([0.0, 1.0, 2.0], "m")
135+
y = UnicodePlot.quantity([0.0, 1.0, 4.0], "m s⁻¹")
136+
p = UnicodePlot.lineplot(x, y)
137+
p.xlabel.should eq("m")
138+
p.ylabel.should eq("m s⁻¹")
139+
end
140+
141+
it "raises on mixed units within one axis" do
142+
x = [UnicodePlot.quantity(0.0, "m"), UnicodePlot.quantity(1.0, "cm")]
143+
y = [0.0, 1.0]
144+
expect_raises(ArgumentError, /mixed units/) do
145+
UnicodePlot.lineplot(x, y)
146+
end
147+
end
132148
end
133149

134150
describe "scatterplot" do
@@ -186,6 +202,12 @@ describe UnicodePlot do
186202
UnicodePlot.scatterplot!(p, [4, 5, 6])
187203
p.series.should eq(prev_series + 1)
188204
end
205+
206+
it "auto-applies y label from quantity y-only input" do
207+
y = UnicodePlot.quantity([22.0, 23.0, 24.0], "°C")
208+
p = UnicodePlot.scatterplot(y)
209+
p.ylabel.should eq("°C")
210+
end
189211
end
190212

191213
describe "barplot" do

src/unicode_plot/common.cr

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
module UnicodePlot
2+
struct Quantity
3+
getter value : Float64
4+
getter unit : String
5+
6+
def initialize(value : Number, @unit : String)
7+
@value = value.to_f64
8+
end
9+
end
10+
211
BLANK = 0x0020_u32
312
BLANK_BRAILLE = 0x2800_u32
413
FULL_BRAILLE = 0x28ff_u32
@@ -176,6 +185,45 @@ module UnicodePlot
176185
marker[0]
177186
end
178187

188+
def quantity(value : Number, unit : String) : Quantity
189+
Quantity.new(value, unit)
190+
end
191+
192+
def quantity(values : Array(T), unit : String) : Array(Quantity) forall T
193+
{% unless T <= Number %}
194+
{% raise "quantity(values, unit) requires numeric array elements" %}
195+
{% end %}
196+
values.map { |v| Quantity.new(v.to_f64, unit) }
197+
end
198+
199+
def unit_label(label : String, unit : String?) : String
200+
stripped = label.rstrip
201+
return stripped unless unit
202+
unit_suffix = "(#{unit})"
203+
return stripped if stripped.ends_with?(unit_suffix)
204+
stripped.empty? ? unit : "#{stripped} (#{unit})"
205+
end
206+
207+
def number_unit(values : Array(Quantity)) : {Array(Float64), String?}
208+
return {[] of Float64, nil} if values.empty?
209+
unit = values.first.unit
210+
nums = Array(Float64).new(values.size)
211+
values.each do |quantity|
212+
unless quantity.unit == unit
213+
raise ArgumentError.new("mixed units are not supported in one axis: #{unit} and #{quantity.unit}")
214+
end
215+
nums << quantity.value
216+
end
217+
{nums, unit}
218+
end
219+
220+
def number_unit(values : Array(T)) : {Array(Float64), String?} forall T
221+
{% unless T <= Number %}
222+
{% raise "number_unit requires numeric array elements" %}
223+
{% end %}
224+
{to_plot_f64(values), nil}
225+
end
226+
179227
private def to_plot_f64(values : Array(T)) : Array(Float64) forall T
180228
{% unless T <= Number %}
181229
{% raise "to_plot_f64 requires numeric array elements" %}

0 commit comments

Comments
 (0)