Skip to content

Commit 423b23d

Browse files
allow combining histogram with visual(Stairs) (#591)
and other plot types such as `Scatter`. Possibly an alternative to #572. The only drawback here is that the `visual(...)` needs to come before `histogram()`, otherwise an error is thrown because `width` is not mapped for other plot types. What do you think of this approach? --------- Co-authored-by: Julius Krumbiegel <[email protected]>
1 parent ef307ba commit 423b23d

File tree

6 files changed

+82
-35
lines changed

6 files changed

+82
-35
lines changed

Diff for: CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Added `plottype` argument to `histogram` to allow for different plot types [#591](https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/591).
6+
57
## v0.9.5 - 2025-03-14
68

79
- Added `mergeable(layer.plottype, layer.primary)` function, intended for extension by third-party packages that define recipes [#592](https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/592).
@@ -121,7 +123,7 @@
121123

122124
## v0.6.11 - 2022-08-08
123125

124-
- Added `paginate` for pagination of large facet plots.
126+
- Added `paginate` for pagination of large facet plots.
125127

126128
## v0.6.8 - 2022-06-14
127129

Diff for: docs/src/examples/statistical-analyses/histograms.md

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ plt = data(df) * mapping(:x, color=:c, stack=:c) * histogram(bins=20)
1515
fg = draw(plt)
1616
````
1717

18+
````@example histograms
19+
df = (x=randn(1000), c=rand(["a", "b"], 1000))
20+
plt = data(df) * mapping(:x, color=:c, linestyle=:c) * histogram(Stairs; bins=20)
21+
fg = draw(plt)
22+
````
23+
1824
````@example histograms
1925
df = (x=rand(1000), y=randn(1000))
2026
plt = data(df) * mapping(:x, :y) * histogram(bins=20)

Diff for: src/transformations/histogram.jl

+45-18
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,68 @@ function _histogram(vs::Tuple; bins=sturges(length(vs[1])), weights=automatic,
3434
return normalize(h, mode=normalization)
3535
end
3636

37-
Base.@kwdef struct HistogramAnalysis{D, B}
38-
datalimits::D=automatic
39-
bins::B=automatic
40-
closed::Symbol=:left
41-
normalization::Symbol=:none
37+
struct HistogramAnalysis{plottype <: Plot, D, B}
38+
datalimits::D
39+
bins::B
40+
closed::Symbol
41+
normalization::Symbol
4242
end
4343

44-
function (h::HistogramAnalysis)(input::ProcessedLayer)
44+
function HistogramAnalysis{plottype}(;
45+
datalimits::D=automatic,
46+
bins::B=automatic,
47+
closed::Symbol=:left,
48+
normalization::Symbol=:none,
49+
) where {plottype <: Plot, D, B}
50+
return HistogramAnalysis{plottype, D, B}(datalimits, bins, closed, normalization)
51+
end
52+
HistogramAnalysis(; options...) = HistogramAnalysis{Plot{plot}}(; options...)
53+
54+
histogram_preprocess_named(::Type{<:Plot}, edges, weights) = (;)
55+
histogram_preprocess_named(::Type{BarPlot}, edges, weights) = (; width=diff(first(edges)))
56+
histogram_preprocess_positional(::Type{<:Plot}, edges, weights) = (map(midpoints, edges)..., weights)
57+
function histogram_preprocess_positional(::Type{Stairs}, edges, weights)
58+
edges = only(edges)
59+
phantomedge = edges[end] # to bring step back to baseline
60+
edges = vcat(edges, phantomedge)
61+
z = zero(eltype(weights))
62+
heights = vcat(z, weights, z)
63+
return (edges, heights)
64+
end
65+
histogram_default_attributes(::Type{<:Plot}) = NamedArguments()
66+
histogram_default_attributes(::Type{BarPlot}) = NamedArguments((; :gap => 0, :dodge_gap => 0))
67+
68+
function (h::HistogramAnalysis{_plottype})(input::ProcessedLayer) where {_plottype}
4569
datalimits = h.datalimits === automatic ? defaultdatalimits(input.positional) : h.datalimits
4670
options = valid_options(; datalimits, h.bins, h.closed, h.normalization)
4771

72+
N = length(input.positional)
73+
default_plottype = categoricalplottypes[N]
74+
plottype = Makie.plottype(_plottype, input.plottype, default_plottype)
75+
4876
output = map(input) do p, n
4977
hist = _histogram(Tuple(p); pairs(n)..., pairs(options)...)
5078
edges, weights = hist.edges, hist.weights
51-
named = length(edges) == 1 ? (; width=diff(first(edges))) : (;)
52-
return (map(midpoints, edges)..., weights), named
79+
named = histogram_preprocess_named(plottype, edges, weights)
80+
positional = histogram_preprocess_positional(plottype, edges, weights)
81+
return positional, named
5382
end
5483

55-
N = length(input.positional)
5684
label = h.normalization == :none ? "count" : string(h.normalization)
5785
labels = set(output.labels, N+1 => label)
58-
attributes = if N == 1
59-
set(output.attributes, :gap => 0, :dodge_gap => 0)
60-
else
61-
output.attributes
62-
end
63-
default_plottype = categoricalplottypes[N]
64-
plottype = Makie.plottype(output.plottype, default_plottype)
86+
attributes = merge(output.attributes, histogram_default_attributes(plottype))
6587
return ProcessedLayer(output; plottype, labels, attributes)
6688
end
6789

6890
"""
69-
histogram(; bins=automatic, datalimits=automatic, closed=:left, normalization=:none)
91+
histogram(plottype::Type{<:Plot} = Plot{plot}; bins=automatic, datalimits=automatic, closed=:left, normalization=:none)
7092
7193
Compute a histogram.
7294
95+
A plot type can be passed as the first argument controlling the type of plot the histogram
96+
is displayed as, e.g. `histogram(Stairs)` creates a stephist. The default plot type for
97+
1-dimensional histograms is `BarPlot`, `Heatmap` for 2d, and `Volume` for 3d histograms.
98+
7399
The attribute `bins` can be an `Integer`, an `AbstractVector` (in particular, a range), or
74100
a `Tuple` of either integers or abstract vectors (useful for 2- or 3-dimensional histograms).
75101
When `bins` is an `Integer`, it denotes the approximate number of equal-width
@@ -97,5 +123,6 @@ Weighted data is supported via the keyword `weights` (passed to `mapping`).
97123
98124
Normalizations are computed withing groups. For example, in the case of
99125
`normalization=:pdf`, sum of weights *within each group* will be equal to `1`.
126+
100127
"""
101-
histogram(; options...) = transformation(HistogramAnalysis(; options...))
128+
histogram(plottype::Type{<:Plot} = Plot{plot}; options...) = transformation(HistogramAnalysis{plottype}(; options...))

Diff for: test/reference_tests.jl

+28-16
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ reftest("barplot cat color palette") do
2727
draw(scales(Color = (; palette = :Set1_3)))
2828
end
2929

30-
reftest("barplot layout") do
30+
reftest("barplot layout") do
3131
data((; x = ["A", "B", "C"], y = 1:3, z = ["X", "Y", "Z"])) * mapping(:x, :y; layout = :z) * visual(BarPlot) |> draw
3232
end
3333

@@ -244,6 +244,18 @@ reftest("histogram stack") do
244244
draw(specs)
245245
end
246246

247+
reftest("histogram stairs cat color") do
248+
df = (x=[sin.(1:500); sin.(1:500) .* 2], z=repeat(["a", "b"], inner = 500))
249+
specs = data(df) * mapping(:x, layout=:z, color = :z) * histogram(Stairs) * visual(alpha = 0.8, linewidth = 4)
250+
draw(specs)
251+
end
252+
253+
reftest("histogram scatter cat color") do
254+
df = (x=[sin.(1:500); sin.(1:500) .* 2], z=repeat(["a", "b"], inner = 500))
255+
specs = data(df) * mapping(:x, layout=:z, color = :z) * histogram(Scatter) * visual(alpha = 0.8, marker = :cross, markersize = 20)
256+
draw(specs)
257+
end
258+
247259
reftest("histogram 2d") do
248260
df = (x=sin.(1:300), y=cos.(1:300))
249261
specs = data(df) * mapping(:x, :y) * histogram()
@@ -736,15 +748,15 @@ function presorted_plot(; with_missing::Bool)
736748
end
737749
group = ["2", "3", "1", "1", "3", "2"]
738750
some_value = sort(exp.(sin.(1:6)))
739-
751+
740752
df = (; countries, group, some_value)
741753

742754
m1 = mapping(:countries, :some_value, color = :group)
743755
m2 = mapping(:countries => presorted, :some_value, color = :group)
744756
m3 = mapping(:countries => presorted, :some_value, color = :group => presorted)
745757

746758
base = data(df) * visual(BarPlot, direction = :x)
747-
759+
748760
f = Figure()
749761
fg1 = draw!(f[1, 1], base * m1)
750762
fg2 = draw!(f[2, 1], base * m2)
@@ -795,7 +807,7 @@ reftest("categorical color from continuous") do
795807
f
796808
end
797809

798-
reftest("title subtitle footnotes") do
810+
reftest("title subtitle footnotes") do
799811
spec = pregrouped(
800812
fill(1:5, 6),
801813
fill(11:15, 6),
@@ -816,7 +828,7 @@ reftest("title subtitle footnotes") do
816828
)
817829
end
818830

819-
reftest("title subtitle footnotes single unconstrained facet") do
831+
reftest("title subtitle footnotes single unconstrained facet") do
820832
spec = data((; x = 1:10, y = 11:20)) * mapping(:x, :y) * visual(Scatter)
821833
draw(
822834
spec;
@@ -831,7 +843,7 @@ reftest("title subtitle footnotes single unconstrained facet") do
831843
)
832844
end
833845

834-
reftest("title") do
846+
reftest("title") do
835847
spec = pregrouped(
836848
fill(1:5, 6),
837849
fill(11:15, 6),
@@ -847,7 +859,7 @@ reftest("title") do
847859
)
848860
end
849861

850-
reftest("title subtitle footnotes settings") do
862+
reftest("title subtitle footnotes settings") do
851863
spec = pregrouped(
852864
fill(1:5, 6),
853865
fill(11:15, 6),
@@ -877,10 +889,10 @@ reftest("title subtitle footnotes settings") do
877889
),
878890
axis = (; width = 100, height = 100)
879891
)
880-
892+
881893
end
882894

883-
reftest("title subtitle footnotes fontsize inherit") do
895+
reftest("title subtitle footnotes fontsize inherit") do
884896
spec = pregrouped(
885897
fill(1:5, 6),
886898
fill(11:15, 6),
@@ -902,7 +914,7 @@ reftest("title subtitle footnotes fontsize inherit") do
902914
)
903915
end
904916

905-
reftest("dodge barplot with errorbars") do
917+
reftest("dodge barplot with errorbars") do
906918
f = Figure()
907919
df = (
908920
x = [1, 1, 2, 2],
@@ -960,7 +972,7 @@ reftest("dodge scatter with rangebars") do
960972
yhigh = cos.(range(0, 2pi, length = 20)) .+ 0.3,
961973
dodge = repeat(["A", "B"], 10)
962974
)
963-
975+
964976
f = Figure()
965977
spec1 = data(df) * (mapping(:x, :y, dodge_x = :dodge, color = :dodge) * visual(Scatter) + mapping(:x, :ylow, :yhigh, dodge_x = :dodge, color = :dodge) * visual(Rangebars))
966978
spec2 = data(df) * (mapping(:y, :x, dodge_y = :dodge, color = :dodge) * visual(Scatter) + mapping(:x, :ylow, :yhigh, dodge_y = :dodge, color = :dodge) * visual(Rangebars, direction = :x))
@@ -974,10 +986,10 @@ end
974986
reftest("manual legend labels in visual") do
975987
df_subjects = (; x = repeat(1:10, 10), y = cos.(1:100), id = repeat(1:10, inner = 10))
976988
df_func = (; x = range(1, 10, length = 20), y = cos.(range(1, 10, length = 20)))
977-
989+
978990
spec1 = data(df_subjects) * mapping(:x, :y, group = :id => nonnumeric) * visual(Lines, linestyle = :dash, color = (:black, 0.2), label = "Subject data")
979991
spec2 = data(df_func) * mapping(:x, :y) * (visual(Lines, color = :tomato) + visual(Scatter, markersize = 12, color = :tomato, strokewidth = 2)) * visual(label = L"\cos(x)")
980-
992+
981993
draw(spec1 + spec2)
982994
end
983995

@@ -986,7 +998,7 @@ reftest("manual legend order") do
986998
spec1 = data(df) * mapping(:x, :y, color = :group) * visual(Lines)
987999

9881000
spec2 = data((; x = 1:10, y = cos.(1:10) .+ 2)) * mapping(:x, :y) * visual(Scatter, color = :purple, label = "Scatter")
989-
1001+
9901002
f = Figure()
9911003
fg = draw!(f[1, 1], spec1 + spec2)
9921004
legend!(f[1, 2], fg)
@@ -1130,7 +1142,7 @@ let
11301142
end
11311143
end
11321144

1133-
let
1145+
let
11341146
df = (;
11351147
wres = 1:10,
11361148
age = 11:20,
@@ -1154,7 +1166,7 @@ let
11541166
end
11551167
end
11561168

1157-
let
1169+
let
11581170
df = (
11591171
x = repeat(1:10, 36),
11601172
y = cumsum(sin.(range(0, 10pi, length = 360))),
10.4 KB
Loading
10.3 KB
Loading

0 commit comments

Comments
 (0)