Skip to content

Commit 2b0a5f9

Browse files
committed
Match Julia histogram behavior
- use Julia-compatible histogram step thresholds - fix fractional bar glyph rendering - keep custom histogram xlabel unchanged - remove unused VHistCanvas - add histogram reference tests
1 parent 030837f commit 2b0a5f9

4 files changed

Lines changed: 77 additions & 11 deletions

File tree

spec/reference_spec.cr

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,59 @@ describe "Julia reference output compatibility" do
382382
p = UnicodePlot.histogram([0.1_f32, 0.1_f32, 0.0_f32])
383383
test_ref("histogram/float32.txt", p)
384384
end
385+
386+
# The following cases reuse DENSITYPLOT_X which is identical to Julia's
387+
# `seed!(RNG, 1337); x = randn(RNG, 4000)` used in tst_histogram.jl.
388+
389+
it "matches histogram/default" do
390+
p = UnicodePlot.histogram(DENSITYPLOT_X)
391+
test_ref("histogram/default.txt", p)
392+
end
393+
394+
it "matches histogram/default_1e-2 (data scaled ×0.01)" do
395+
p = UnicodePlot.histogram(DENSITYPLOT_X.map { |v| v * 0.01 })
396+
test_ref("histogram/default_1e-2.txt", p)
397+
end
398+
399+
it "matches histogram/default_1e2 (data scaled ×100)" do
400+
p = UnicodePlot.histogram(DENSITYPLOT_X.map { |v| v * 100.0 })
401+
test_ref("histogram/default_1e2.txt", p)
402+
end
403+
404+
it "matches histogram/log10 (xscale: :log10)" do
405+
p = UnicodePlot.histogram(DENSITYPLOT_X, xscale: :log10)
406+
test_ref("histogram/log10.txt", p)
407+
end
408+
409+
it "matches histogram/log10_label (xscale: :log10 with custom xlabel)" do
410+
p = UnicodePlot.histogram(DENSITYPLOT_X, xlabel: "custom label", xscale: :log10)
411+
test_ref("histogram/log10_label.txt", p)
412+
end
413+
414+
it "matches histogram/parameters1 (title, xlabel, color:blue, margin, padding)" do
415+
p = UnicodePlot.histogram(
416+
DENSITYPLOT_X,
417+
title: "My Histogram",
418+
xlabel: "Absolute Frequency",
419+
color: :blue,
420+
margin: 7,
421+
padding: 3,
422+
)
423+
test_ref("histogram/parameters1.txt", p)
424+
end
425+
426+
it "matches histogram/parameters2 (title, color:yellow, border:solid, symbols:[=], width:50)" do
427+
p = UnicodePlot.histogram(
428+
DENSITYPLOT_X,
429+
title: "My Histogram",
430+
xlabel: "Absolute Frequency",
431+
color: :yellow,
432+
border: :solid,
433+
symbols: ['='],
434+
width: 50,
435+
)
436+
test_ref("histogram/parameters2.txt", p)
437+
end
385438
end
386439

387440
describe "boxplot" do

spec/unicode_plot_spec.cr

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,14 @@ describe UnicodePlot do
214214
p_right.to_s.should be_a(String)
215215
end
216216

217-
it "applies xscale and reflects transformed xlabel" do
217+
it "applies xscale; custom xlabel used as-is (no scale suffix), default xlabel gets suffix" do
218+
# Custom xlabel: no [log10] suffix (matches Julia behavior)
218219
p = UnicodePlot.histogram([1.0, 10.0, 100.0], xscale: :log10, xlabel: "x")
219-
p.to_s.should contain("x [log10]")
220+
p.to_s.should contain("x")
221+
p.to_s.should_not contain("x [log10]")
222+
# Default xlabel: gets [log10] suffix
223+
p2 = UnicodePlot.histogram([1.0, 10.0, 100.0], xscale: :log10)
224+
p2.to_s.should contain("Frequency [log10]")
220225
end
221226

222227
it "accepts RGB tuple color" do

src/unicode_plot/graphics/bar_graphics.cr

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,13 @@ module UnicodePlot
112112
UnicodePlot.print_color(io, color, @symbols[nsyms - 1].to_s * bar_head, use_color)
113113
if nsyms > 1
114114
frac_cells = frac * max_bar_width - bar_head
115-
sub_idx = (frac_cells * nsyms).round.to_i32
116-
sub_char = sub_idx <= 0 ? ' ' : @symbols[(sub_idx - 1).clamp(0, nsyms - 1)]
115+
rem = frac_cells * (nsyms - 2)
116+
sub_char = if rem <= 0
117+
' '
118+
else
119+
ri = rem.round.to_i32
120+
@symbols[ri.clamp(0, nsyms - 2)]
121+
end
117122
UnicodePlot.print_color(io, color, sub_char.to_s, use_color)
118123
bar_head += 1
119124
end

src/unicode_plot/interface/histogram.cr

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@ module UnicodePlot
44
(Math.log2(n) + 1).ceil.to_i32
55
end
66

7-
# Round step up to next "nice" value ({1,2,2.5,5} × 10^k), matching Julia's histrange.
7+
# Round step up to next "nice" value ({1,2,5} × 10^k), matching Julia's StatsBase histrange.
8+
# Thresholds 1.1, 2.2, 5.5 are taken directly from StatsBase._histrange_step.
89
private def nice_hist_step(step : Float64) : Float64
910
return step if step <= 0.0
1011
mag = 10.0 ** Math.log10(step).floor
1112
k = step / mag
12-
n = if k <= 1.0
13+
n = if k <= 1.1
1314
1.0
14-
elsif k <= 2.0
15+
elsif k <= 2.2
1516
2.0
16-
elsif k <= 2.5
17-
2.5
18-
elsif k <= 5.0
17+
elsif k <= 5.5
1918
5.0
2019
else
2120
10.0
@@ -293,7 +292,7 @@ module UnicodePlot
293292
pr2 = " " * (pad_right - (s2.size - dot2))
294293
"#{l_chr}#{pl1}#{s1}#{pr1}, #{pl2}#{s2}#{pr2}#{r_chr}"
295294
end
296-
barplot(
295+
plot = barplot(
297296
labels_arr, counts,
298297
color: color, title: title,
299298
xlabel: xlabel.empty? ? "Frequency" : xlabel,
@@ -303,6 +302,10 @@ module UnicodePlot
303302
thousands_separator: thousands_separator, symbols: symbols,
304303
width: width
305304
)
305+
# When a custom xlabel is provided, use it as-is (no scale-name suffix),
306+
# matching Julia's behavior where kw... overrides the default transform_name label.
307+
plot.xlabel = xlabel unless xlabel.empty?
308+
plot
306309
end
307310

308311
def histogram(data : Array(T), **kwargs) : Plot forall T

0 commit comments

Comments
 (0)