Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add labels to barplots + hist #1069

Merged
merged 8 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/src/plotting_functions/barplot.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,47 @@ Legend(fig[1,2], elements, labels, title)

fig
```

```@example bar
barplot(
tbl.x, tbl.height,
dodge = tbl.grp,
color = tbl.grp,
bar_labels = :y,
axis = (xticks = (1:3, ["left", "middle", "right"]),
title = "Dodged bars horizontal with labels"),
colormap = [:red, :green, :blue],
color_over_background=:red,
color_over_bar=:white,
flip_labels_at=0.85,
direction=:x,
)
```

```@example bar
barplot(
tbl.x, tbl.height,
dodge = tbl.grp,
color = tbl.grp,
bar_labels = :y,
axis = (xticks = (1:3, ["left", "middle", "right"]),
title = "Dodged bars horizontal with labels"),
colormap = [:red, :green, :blue],
color_over_background=:red,
color_over_bar=:white,
flip_labels_at=0.85,
direction=:x,
)
```

```@example bar
barplot([-1, -0.5, 0.5, 1],
bar_labels = :y,
axis = (title="Fonts + flip_labels_at",),
label_size = 20,
flip_labels_at=(-0.8, 0.8),
label_color=[:white, :green, :black, :white],
label_formatter = x-> "Flip at $(x)?",
label_offset = 10
)
```
13 changes: 12 additions & 1 deletion docs/src/plotting_functions/hist.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ hist

### Examples

```@example
```@example hist
using GLMakie
GLMakie.activate!() # hide
Makie.inline!(true) # hide
Expand All @@ -20,3 +20,14 @@ hist(f[2, 1], data, bins = [-5, -2, -1, 0, 1, 2, 5], color = :gray)
hist(f[2, 2], data, normalization = :pdf)
f
```

#### Histogram with labels

You can use all the same arguments as [`barplot`](@ref):
```@example hist
using CairoMakie
CairoMakie.activate!()
hist(data, normalization = :pdf, bar_labels = :values,
label_formatter=x-> round(x, digits=2), label_size = 15,
strokewidth = 0.5, strokecolor = (:black, 0.5), color = :values)
```
2 changes: 1 addition & 1 deletion src/Makie.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ using FixedPointNumbers, Packing, SignedDistanceFields
using Markdown, DocStringExtensions # documentation
using Serialization # serialize events
using StructArrays
using GeometryBasics: widths, positive_widths, VecTypes, AbstractPolygon
using GeometryBasics: widths, positive_widths, VecTypes, AbstractPolygon, value
using StaticArrays
import StatsBase, Distributions, KernelDensity
using Distributions: Distribution, VariateForm, Discrete, QQPair, pdf, quantile, qqbuild
Expand Down
209 changes: 150 additions & 59 deletions src/basic_recipes/barplot.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
function bar_label_formatter(value::Number)
return string(round(value; digits=3))
end

"""
barplot(x, y; kwargs...)

Expand Down Expand Up @@ -25,17 +29,28 @@ $(ATTRIBUTES)
visible = theme(scene, :visible),
inspectable = theme(scene, :inspectable),
cycle = [:color => :patchcolor],

bar_labels = nothing,
flip_labels_at = Inf,
label_color = theme(scene, :textcolor),
color_over_background = automatic,
color_over_bar = automatic,
label_offset = 5,
label_font = theme(scene, :font),
label_size = 20,
label_formatter = bar_label_formatter
)
end

conversion_trait(::Type{<: BarPlot}) = PointBased()

function bar_rectangle(x, y, width, fillto)
function bar_rectangle(x, y, width, fillto, in_y_direction)
# y could be smaller than fillto...
ymin = min(fillto, y)
ymax = max(fillto, y)
w = abs(width)
return FRect(x - (w / 2f0), ymin, w, ymax - ymin)
rect = FRect(x - (w / 2f0), ymin, w, ymax - ymin)
return in_y_direction ? rect : flip(rect)
end

flip(r::Rect2D) = Rect2D(reverse(origin(r)), reverse(widths(r)))
Expand All @@ -55,19 +70,123 @@ function xw_from_dodge(x, width, minimum_distance, x_gap, dodge, n_dodge, dodge_
return x .+ width .* shifts, width * dodge_width
end

function Makie.plot!(p::BarPlot)
scale_width(dodge_gap, n_dodge) = (1 - (n_dodge - 1) * dodge_gap) / n_dodge

function shift_dodge(i, dodge_width, dodge_gap)
(dodge_width - 1) / 2 + (i - 1) * (dodge_width + dodge_gap)
end

function stack_from_to_sorted(y)
to = cumsum(y)
from = [0.0; to[firstindex(to):end-1]]

(from = from, to = to)
end

function stack_from_to(i_stack, y)
# save current order
order = 1:length(y)
# sort by i_stack
perm = sortperm(i_stack)
# restore original order
inv_perm = sortperm(order[perm])

from, to = stack_from_to_sorted(view(y, perm))

(from = view(from, inv_perm), to = view(to, inv_perm))
end

function stack_grouped_from_to(i_stack, y, grp)

from = Array{Float64}(undef, length(y))
to = Array{Float64}(undef, length(y))

in_y_direction = lift(p.direction) do dir
if dir == :y
true
elseif dir == :x
false
groupby = StructArray((; grp..., is_pos = y .> 0))

grps = StructArrays.finduniquesorted(groupby)

for (grp, inds) in grps

fromto = stack_from_to(i_stack[inds], y[inds])

from[inds] .= fromto.from
to[inds] .= fromto.to

end

(from = from, to = to)
end

function text_attributes(values, in_y_direction, flip_labels_at, color_over_background, color_over_bar, label_offset)
aligns = Vec2f0[]
offsets = Vec2f0[]
text_colors = RGBAf0[]
swap(x, y) = in_y_direction ? (x, y) : (y, x)
geti(x::AbstractArray, i) = x[i]
geti(x, i) = x
function flip(k)
if flip_labels_at isa Number
return k > flip_labels_at || k < 0
elseif flip_labels_at isa Tuple{<:Number, <: Number}
return (k > flip_labels_at[2] || k < 0) && k > flip_labels_at[1]
else
error("Invalid direction $dir. Options are :x and :y.")
error("flip_labels_at needs to be a tuple of two numbers (low, high), or a single number (high)")
end
end

for (i, k) in enumerate(values)
# Plot text inside bar
if flip(k)
push!(aligns, swap(0.5, 1.0))
push!(offsets, swap(0, -label_offset))
push!(text_colors, geti(color_over_bar, i))
else
# plot text next to bar
push!(aligns, swap(0.5, 0.0))
push!(offsets, swap(0, label_offset))
push!(text_colors, geti(color_over_background, i))
end
end
return aligns, offsets, text_colors
end

function barplot_labels(xpositions, ypositions, bar_labels, in_y_direction, flip_labels_at, color_over_background, color_over_bar, label_formatter, label_offset)
if bar_labels isa Symbol && bar_labels in (:x, :y)
bar_labels = map(xpositions, ypositions) do x, y
if bar_labels === :x
label_formatter.(x)
else
label_formatter.(y)
end
end
end
if bar_labels isa AbstractVector
if length(bar_labels) == length(xpositions)
attributes = text_attributes(ypositions, in_y_direction, flip_labels_at, color_over_background, color_over_bar, label_offset)
label_pos = map(xpositions, ypositions, bar_labels) do x, y, l
return (string(l), in_y_direction ? Point2f0(x, y) : Point2f0(y, x))
end
return (label_pos, attributes...)
else
error("Labels and bars need to have same length. Found: $(length(xpositions)) bars with these labels: $(bar_labels)")
end
else
error("Unsupported label type: $(typeof(bar_labels)). Use: :x, :y, or a vector of values that can be converted to strings.")
end
end

bars = lift(p[1], p.fillto, p.width, p.dodge, p.n_dodge, p.x_gap, p.dodge_gap, p.stack, in_y_direction) do xy, fillto, width, dodge, n_dodge, x_gap, dodge_gap, stack, in_y_direction
function Makie.plot!(p::BarPlot)
labels = Observable(Tuple{String, Point2f0}[])
label_aligns = Observable(Vec2f0[])
label_offsets = Observable(Vec2f0[])
label_colors = Observable(RGBAf0[])
function calculate_bars(xy, fillto, width, dodge, n_dodge, x_gap, dodge_gap, stack,
dir, bar_labels, flip_labels_at, label_color, color_over_background,
color_over_bar, label_formatter, label_offset)

in_y_direction = get((y=true, x=false), dir) do
error("Invalid direction $dir. Options are :x and :y.")
end

x = first.(xy)
y = last.(xy)
Expand Down Expand Up @@ -101,60 +220,32 @@ function Makie.plot!(p::BarPlot)
ArgumentError("The keyword argument `stack` currently supports only `AbstractVector{<: Integer}`") |> throw
end

rects = @. bar_rectangle(x̂, y, barwidth, fillto)
return in_y_direction ? rects : flip.(rects)
# --------------------------------
# ----------- Labels -------------
# --------------------------------

if !isnothing(bar_labels)
oback = color_over_background === automatic ? label_color : color_over_background
obar = color_over_bar === automatic ? label_color : color_over_bar
label_args = barplot_labels(x̂, y, bar_labels, in_y_direction,
flip_labels_at, to_color(oback), to_color(obar),
label_formatter, label_offset)
labels[], label_aligns[], label_offsets[], label_colors[] = label_args
end

return bar_rectangle.(x̂, y, barwidth, fillto, in_y_direction)
end

bars = lift(calculate_bars, p[1], p.fillto, p.width, p.dodge, p.n_dodge, p.x_gap,
p.dodge_gap, p.stack, p.direction, p.bar_labels, p.flip_labels_at,
p.label_color, p.color_over_background, p.color_over_bar, p.label_formatter, p.label_offset)

poly!(
p, bars, color = p.color, colormap = p.colormap, colorrange = p.colorrange,
strokewidth = p.strokewidth, strokecolor = p.strokecolor, visible = p.visible,
inspectable = p.inspectable
)
end

scale_width(dodge_gap, n_dodge) = (1 - (n_dodge - 1) * dodge_gap) / n_dodge

function shift_dodge(i, dodge_width, dodge_gap)
(dodge_width - 1) / 2 + (i - 1) * (dodge_width + dodge_gap)
end

function stack_grouped_from_to(i_stack, y, grp)

from = Array{Float64}(undef, length(y))
to = Array{Float64}(undef, length(y))

groupby = StructArray((; grp..., is_pos = y .> 0))

grps = StructArrays.finduniquesorted(groupby)

for (grp, inds) in grps

fromto = stack_from_to(i_stack[inds], y[inds])

from[inds] .= fromto.from
to[inds] .= fromto.to

end

(from = from, to = to)
end

function stack_from_to(i_stack, y)
# save current order
order = 1:length(y)
# sort by i_stack
perm = sortperm(i_stack)
# restore original order
inv_perm = sortperm(order[perm])

from, to = stack_from_to_sorted(view(y, perm))

(from = view(from, inv_perm), to = view(to, inv_perm))
end

function stack_from_to_sorted(y)
to = cumsum(y)
from = [0.0; to[firstindex(to):end-1]]

(from = from, to = to)
if !isnothing(p.bar_labels[])
text!(p, labels; align=label_aligns, offset=label_offsets, color=label_colors, font=p.label_font, textsize=p.label_size)
end
end
1 change: 0 additions & 1 deletion src/layouting/layouting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ function glyph_positions(str::AbstractString, font_per_char, fontscale_px, halig
else
error("Invalid halign $halign. Valid values are <:Number, :left, :center and :right.")
end

xs_aligned = [xsgroup .- halign * maxwidth for xsgroup in xs_justified]

# for y alignment, we need the largest ascender of the first line
Expand Down
30 changes: 28 additions & 2 deletions src/stats/hist.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ can be normalized by setting `normalization`. Possible values are:
norm 1.
* `:none`: Do not normalize.

Color can either be:
* a vector of `bins` colors
* a single color
* `:values`, to color the bars with the values from the histogram

## Attributes
$(ATTRIBUTES)
"""
Expand All @@ -36,12 +41,23 @@ $(ATTRIBUTES)
bins = 15, # Int or iterable of edges
normalization = :none,
cycle = [:color => :patchcolor],
color = theme(scene, :patchcolor),

bar_labels = nothing,
flip_labels_at = Inf,
label_color = theme(scene, :textcolor),
over_background_color = automatic,
over_bar_color = automatic,
label_offset = 5,
label_font = theme(scene, :font),
label_size = 20,
label_formatter = bar_label_formatter
)
end

function Makie.plot!(plot::Hist)

values = plot[:values]
values = plot.values

edges = lift(values, plot.bins) do vals, bins
if bins isa Int
Expand All @@ -65,9 +81,19 @@ function Makie.plot!(plot::Hist)
end

widths = lift(diff, edges)
color = lift(plot.color) do color
if color === :values
return last.(points[])
else
return color
end
end

bar_labels = map(plot.bar_labels) do x
x === :values ? :y : x
end
# plot the values, not the observables, to be in control of updating
bp = barplot!(plot, points[]; width = widths[], plot.attributes...)
bp = barplot!(plot, points[]; width = widths[], plot.attributes..., bar_labels=bar_labels, color=color)

# update the barplot points without triggering, then trigger with `width`
on(widths) do w
Expand Down