Skip to content

Commit fc00115

Browse files
committed
Add polar
1 parent b4a211f commit fc00115

8 files changed

Lines changed: 446 additions & 13 deletions

File tree

examples/polarplot.cr

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
require "../src/unicode_plot"
2+
3+
include UnicodePlot
4+
5+
# Basic spiral-like polar line
6+
theta = (0...40).map { |i| 4.0 * Math::PI * i.to_f64 / 39.0 }
7+
r = theta.map { |angle| angle / (2.0 * Math::PI) }
8+
9+
p1 = polarplot(theta, r, title: "Polar line", color: :green)
10+
puts p1
11+
puts
12+
13+
# Polar scatter mode (lines: false)
14+
theta2 = (0...20).map { |i| 2.0 * Math::PI * i.to_f64 / 19.0 }
15+
r2 = (0...20).map { |i| 1.0 + 0.2 * Math.sin(i.to_f64) }
16+
17+
p2 = polarplot(theta2, r2,
18+
lines: false,
19+
color: :red,
20+
marker: :circle,
21+
border: :solid,
22+
title: "Polar scatter")
23+
puts p2
24+
puts
25+
26+
# Explicit radial limit and callable radius
27+
theta3 = (0...50).map { |i| 3.0 * Math::PI * i.to_f64 / 49.0 }
28+
p3 = polarplot(theta3,
29+
->(angle : Float64) { 1.0 + 0.5 * Math.cos(3.0 * angle) },
30+
rlim: {0.0, 2.0},
31+
title: "Callable radius with rlim")
32+
puts p3

spec/fixtures/README.md

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
11
# Fixtures for reference specs
22

3-
This directory contains generated fixture data used by Crystal specs.
3+
This directory contains Julia-generated fixture data consumed by Crystal specs.
4+
Fixtures are intended to be generated, not hand-edited.
45

5-
## Heatmap fixture
6-
7-
- File: `julia_heatmap_data.json`
8-
- Generator: `generate_julia_heatmap_data.jl`
9-
- Purpose: keep heatmap input matrices identical to UnicodePlots.jl reference data.
10-
11-
The heatmap fixture is intentionally generated by Julia (not hand-edited) so Crystal reference tests can compare output with the same numeric input.
12-
13-
## Regenerate
6+
## Quick regenerate
147

158
Run from repository root:
169

1710
```bash
1811
julia spec/fixtures/generate_julia_heatmap_data.jl
12+
julia spec/fixtures/generate_julia_densityplot_data.jl
13+
julia spec/fixtures/generate_julia_spy_data.jl
14+
julia spec/fixtures/generate_julia_polarplot_data.jl
1915
```
2016

21-
This rewrites `julia_heatmap_data.json` with deterministic data using seed `1337`.
17+
## Heatmap
18+
19+
- Data file: `julia_heatmap_data.json`
20+
- Generator: `generate_julia_heatmap_data.jl`
21+
- Purpose: keep heatmap input matrices aligned with UnicodePlots.jl reference inputs.
22+
- Notes: deterministic output (seed `1337`).
23+
- Consumer: `spec/reference_spec.cr`
24+
25+
## Densityplot
26+
27+
- Data file: `julia_densityplot_data.json`
28+
- Generator: `generate_julia_densityplot_data.jl`
29+
- Purpose: keep `x` and `y` samples aligned with UnicodePlots.jl densityplot tests.
30+
- Notes: deterministic output (seed `1337`).
31+
- Consumer: `spec/reference_spec.cr`
32+
33+
## Spy
34+
35+
- Data file: `julia_spy_data.json`
36+
- Generator: `generate_julia_spy_data.jl`
37+
- Purpose: keep sparse matrix fixtures aligned with UnicodePlots.jl spy tests.
38+
- Notes: generated from stable sparse sampling logic in Julia.
39+
- Consumer: `spec/reference_spec.cr`
2240

23-
## Consumer
41+
## Polarplot
2442

25-
- `spec/reference_spec.cr` loads this JSON and caches decoded matrices per key.
43+
- Data file: `julia_polarplot_data.json`
44+
- Generator: `generate_julia_polarplot_data.jl`
45+
- Purpose: keep polarplot input parameters aligned with UnicodePlots.jl tests.
46+
- Notes: stores deterministic linspace parameters (`start`, `stop`, `length`) for `theta` and `r`.
47+
- Consumer: `spec/reference_spec.cr`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generates deterministic polarplot fixtures matching UnicodePlots.jl test inputs.
2+
# This stores linspace parameters so Crystal specs can reconstruct exact arrays.
3+
4+
function write_json(path::String)
5+
open(path, "w") do io
6+
write(io, "{\n")
7+
write(io, " \"simple\": {\n")
8+
write(io, " \"theta\": {\"start\": 0.0, \"stop\": $(repr(2pi)), \"length\": 20},\n")
9+
write(io, " \"r\": {\"start\": 0.0, \"stop\": 2.0, \"length\": 20}\n")
10+
write(io, " },\n")
11+
write(io, " \"simple_with_rlim\": {\n")
12+
write(io, " \"theta\": {\"start\": 0.0, \"stop\": $(repr(2pi)), \"length\": 20},\n")
13+
write(io, " \"r\": {\"start\": 0.0, \"stop\": 2.0, \"length\": 20},\n")
14+
write(io, " \"rlim\": [0.0, 3.0]\n")
15+
write(io, " },\n")
16+
write(io, " \"callable\": {\n")
17+
write(io, " \"theta\": {\"start\": 0.0, \"stop\": $(repr(4pi)), \"length\": 40}\n")
18+
write(io, " },\n")
19+
write(io, " \"kwargs\": {\n")
20+
write(io, " \"theta\": {\"start\": 0.0, \"stop\": $(repr(2pi)), \"length\": 20},\n")
21+
write(io, " \"r\": {\"start\": 0.0, \"stop\": 1.0, \"length\": 20},\n")
22+
write(io, " \"size_scale\": 1.5\n")
23+
write(io, " }\n")
24+
write(io, "}\n")
25+
end
26+
end
27+
28+
out = joinpath(@__DIR__, "julia_polarplot_data.json")
29+
write_json(out)
30+
println("Written: $out")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"simple": {
3+
"theta": {"start": 0.0, "stop": 6.283185307179586, "length": 20},
4+
"r": {"start": 0.0, "stop": 2.0, "length": 20}
5+
},
6+
"simple_with_rlim": {
7+
"theta": {"start": 0.0, "stop": 6.283185307179586, "length": 20},
8+
"r": {"start": 0.0, "stop": 2.0, "length": 20},
9+
"rlim": [0.0, 3.0]
10+
},
11+
"callable": {
12+
"theta": {"start": 0.0, "stop": 12.566370614359172, "length": 40}
13+
},
14+
"kwargs": {
15+
"theta": {"start": 0.0, "stop": 6.283185307179586, "length": 20},
16+
"r": {"start": 0.0, "stop": 1.0, "length": 20},
17+
"size_scale": 1.5
18+
}
19+
}

spec/reference_spec.cr

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ DENSITYPLOT_Y = DENSITYPLOT_FIXTURE_JSON["y"].as_a.map(&.as_f)
1616
SPY_FIXTURE_PATH = File.join(__DIR__, "fixtures", "julia_spy_data.json")
1717
SPY_FIXTURE_JSON = JSON.parse(File.read(SPY_FIXTURE_PATH))
1818

19+
POLARPLOT_FIXTURE_PATH = File.join(__DIR__, "fixtures", "julia_polarplot_data.json")
20+
POLARPLOT_FIXTURE_JSON = JSON.parse(File.read(POLARPLOT_FIXTURE_PATH))
21+
1922
# Returns a matrix fixture by key ("60x60", "10x10", "10x11").
2023
# The parsed matrix is cached because multiple examples reuse the same payload.
2124
def heatmap_fixture_matrix(key : String) : Array(Array(Float64))
@@ -74,6 +77,14 @@ def spy_flip_matrix : Array(Array(Float64))
7477
a
7578
end
7679

80+
def polarplot_fixture_linspace(case_key : String, axis : String) : Array(Float64)
81+
entry = POLARPLOT_FIXTURE_JSON[case_key][axis]
82+
start_v = entry["start"].as_f
83+
stop_v = entry["stop"].as_f
84+
length = entry["length"].as_i
85+
linspace(start_v, stop_v, length)
86+
end
87+
7788
def linspace(start_v : Float64, end_v : Float64, length : Int32) : Array(Float64)
7889
return [] of Float64 if length <= 0
7990
return [start_v] if length == 1
@@ -1239,4 +1250,46 @@ describe "Julia reference output compatibility" do
12391250
end
12401251
end
12411252
end
1253+
1254+
describe "polarplot" do
1255+
it "matches polarplot/simple" do
1256+
theta = polarplot_fixture_linspace("simple", "theta")
1257+
r = polarplot_fixture_linspace("simple", "r")
1258+
p = UnicodePlot.polarplot(theta, r)
1259+
test_ref("polarplot/simple.txt", p)
1260+
end
1261+
1262+
it "matches polarplot/simple_with_rlim" do
1263+
theta = polarplot_fixture_linspace("simple_with_rlim", "theta")
1264+
r = polarplot_fixture_linspace("simple_with_rlim", "r")
1265+
rlim_json = POLARPLOT_FIXTURE_JSON["simple_with_rlim"]["rlim"].as_a
1266+
rlim = {rlim_json[0].as_f, rlim_json[1].as_f}
1267+
p = UnicodePlot.polarplot(theta, r, rlim: rlim)
1268+
test_ref("polarplot/simple_with_rlim.txt", p)
1269+
end
1270+
1271+
it "matches polarplot/callable" do
1272+
theta = polarplot_fixture_linspace("callable", "theta")
1273+
p = UnicodePlot.polarplot(theta, ->(angle : Float64) { angle / (2.0 * Math::PI) })
1274+
test_ref("polarplot/callable.txt", p)
1275+
end
1276+
1277+
it "matches polarplot/kwargs" do
1278+
scale = POLARPLOT_FIXTURE_JSON["kwargs"]["size_scale"].as_f
1279+
h = (UnicodePlot.default_height * scale).round.to_i
1280+
w = (UnicodePlot.default_width * scale).round.to_i
1281+
theta = polarplot_fixture_linspace("kwargs", "theta")
1282+
r = polarplot_fixture_linspace("kwargs", "r")
1283+
p = UnicodePlot.polarplot(
1284+
theta,
1285+
r,
1286+
lines: false,
1287+
border: :solid,
1288+
color: :red,
1289+
height: h,
1290+
width: w,
1291+
)
1292+
test_ref("polarplot/kwargs.txt", p)
1293+
end
1294+
end
12421295
end

spec/unicode_plot_spec.cr

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,53 @@ describe UnicodePlot do
476476
end
477477
end
478478

479+
describe "polarplot" do
480+
it "returns a Plot" do
481+
theta = [0.0, Math::PI / 2.0, Math::PI]
482+
r = [0.5, 1.0, 1.5]
483+
p = UnicodePlot.polarplot(theta, r)
484+
p.should be_a(UnicodePlot::Plot)
485+
end
486+
487+
it "raises on mismatched theta/r lengths" do
488+
expect_raises(ArgumentError, /same length/) do
489+
UnicodePlot.polarplot([0.0, Math::PI], [1.0])
490+
end
491+
end
492+
493+
it "accepts numeric overloads" do
494+
p = UnicodePlot.polarplot([0_i64, 1_i64, 2_i64], [1_i32, 2_i32, 3_i32])
495+
p.to_s.should be_a(String)
496+
end
497+
498+
it "accepts callable radius" do
499+
theta = [0.0, Math::PI / 2.0, Math::PI]
500+
p = UnicodePlot.polarplot(theta, ->(angle : Float64) { angle + 1.0 })
501+
p.to_s.should be_a(String)
502+
end
503+
504+
it "supports scatter mode with lines: false" do
505+
theta = [0.0, Math::PI / 2.0, Math::PI, 3.0 * Math::PI / 2.0]
506+
r = [1.0, 1.0, 1.0, 1.0]
507+
p = UnicodePlot.polarplot(theta, r, lines: false)
508+
p.to_s.should contain("")
509+
p.to_s.should contain("90°")
510+
p.to_s.should contain("180°")
511+
p.to_s.should contain("270°")
512+
end
513+
514+
it "uses 3π / 2 label when degrees is false" do
515+
theta = [0.0, Math::PI / 2.0, Math::PI, 3.0 * Math::PI / 2.0]
516+
r = [1.0, 1.0, 1.0, 1.0]
517+
p = UnicodePlot.polarplot(theta, r, degrees: false)
518+
p.to_s.should contain("0")
519+
p.to_s.should contain("π / 2")
520+
p.to_s.should contain("π")
521+
p.to_s.should contain("3π / 2")
522+
p.to_s.should_not contain("3π / 4")
523+
end
524+
end
525+
479526
describe "stairs" do
480527
it "raises on mismatched x/y lengths" do
481528
expect_raises(ArgumentError, /same length/) do

src/unicode_plot.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require "./unicode_plot/interface/boxplot"
2222
require "./unicode_plot/interface/densityplot"
2323
require "./unicode_plot/interface/heatmap"
2424
require "./unicode_plot/interface/spy"
25+
require "./unicode_plot/interface/polarplot"
2526
require "./unicode_plot/interface/stairs"
2627

2728
module UnicodePlot

0 commit comments

Comments
 (0)