Skip to content

Commit

Permalink
Add Tucan.range_bar/4 plot
Browse files Browse the repository at this point in the history
  • Loading branch information
pnezis committed Sep 4, 2024
1 parent 8b5d401 commit a8e6dee
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 1 deletion.
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@

### Added

- Add `Tucan.Export` wrapper around `VegaLite.Export`
#### Plots

- Add `Tucan.range_bar/4` plot

```tucan
data = [
%{category: "A", min: 28, max: 55},
%{category: "B", min: 43, max: 91},
%{category: "C", min: 13, max: 61}
]
Tucan.range_bar(data, "category", "min", "max", fill_color: "red")
```

- Add `Tucan.Finance.candlestick/7` specialized plot

Expand All @@ -26,6 +38,9 @@ Tucan.layers([
|> Tucan.Scale.set_xy_domain(-7, 7)
```

#### Other

- Add `Tucan.Export` wrapper around `VegaLite.Export`
- Add `Tucan.Axes.set_color/2`, `Tucan.Axes.set_color/3` helpers.
- Add `Tucan.Axes.set_title_color/3` helpers.
- Add `Tucan.Grid.set_color/2` helper.
Expand Down
107 changes: 107 additions & 0 deletions lib/tucan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2142,6 +2142,113 @@ defmodule Tucan do
|> maybe_flip_axes(flip_axes?)
end

@range_bar_opts Tucan.Options.take!([
@global_opts,
@global_mark_opts,
:color_by,
:orient,
:x,
:x2,
:y,
:y2,
:x_offset,
:y_offset,
:color,
:fill_color,
:corner_radius
])
@range_bar_schema Tucan.Options.to_nimble_schema!(@range_bar_opts)

@doc """
Draws a range bar chart.
A range bar chart is a bar chart where the bar is not a single value, but a range defined by a
`min` and `max` value. It is used for showing intervals or ranges of data.
## Options
#{Tucan.Options.docs(@range_bar_opts)}
## Examples
A simple ranged bar chart:
```tucan
data = [
%{"id" => "A", "min" => 28, "max" => 55},
%{"id" => "B", "min" => 43, "max" => 91},
%{"id" => "C", "min" => 13, "max" => 61}
]
Tucan.range_bar(data, "id", "min", "max")
```
You can set a `color_by` option that will create a stacked ranged bar chart:
```tucan
data = [
%{"product" => "Laptop", "category" => "Basic", "min_price" => 499, "max_price" => 799},
%{"product" => "Laptop", "category" => "Regular", "min_price" => 999, "max_price" => 1799},
%{"product" => "Laptop", "category" => "Premium", "min_price" => 1499, "max_price" => 2499},
%{"product" => "Smartphone", "category" => "Basic", "min_price" => 199, "max_price" => 399},
%{"product" => "Smartphone", "category" => "Regular", "min_price" => 399, "max_price" => 699},
%{"product" => "Smartphone", "category" => "Premium", "min_price" => 699, "max_price" => 1299},
%{"product" => "Tablet", "category" => "Basic", "min_price" => 299, "max_price" => 499},
%{"product" => "Tablet", "category" => "Regular", "min_price" => 399, "max_price" => 699},
%{"product" => "Tablet", "category" => "Premium", "min_price" => 599, "max_price" => 1990}
]
Tucan.range_bar(data, "product", "min_price", "max_price",
color_by: "category",
tooltip: true
)
```
You can tweak the look of the chart using the supported options:
```tucan
data = [
%{"id" => "A", "min" => 28, "max" => 55},
%{"id" => "B", "min" => 43, "max" => 91},
%{"id" => "C", "min" => 13, "max" => 61}
]
Tucan.range_bar(data, "id", "min", "max", orient: :vertical, fill_color: "#33245A", corner_radius: 5)
```
"""
@doc section: :plots
@spec range_bar(
plotdata :: plotdata(),
field :: String.t(),
min :: String.t(),
max :: String.t(),
opts :: keyword()
) ::
VegaLite.t()
def range_bar(plotdata, field, min, max, opts \\ []) do
opts = NimbleOptions.validate!(opts, @range_bar_schema)

flip_axes? = opts[:orient] == :vertical
opts = maybe_flip_encoding_options(opts, flip_axes?)

spec_opts = Tucan.Options.take_options(opts, @range_bar_opts, :spec)

mark_opts =
Tucan.Options.take_options(opts, @range_bar_opts, :mark)
|> Tucan.Keyword.put_not_nil(:color, opts[:fill_color])
|> Tucan.Keyword.put_not_nil(:corner_radius_end, opts[:corner_radius])

plotdata
|> new(spec_opts)
|> Vl.mark(:bar, mark_opts)
|> encode_field(:y, field, opts, type: :nominal, axis: [label_angle: 0])
|> encode_field(:x, min, opts, type: :quantitative)
|> encode_field(:x2, max, opts, type: :quantitative)
|> maybe_encode_field(:color, fn -> opts[:color_by] != nil end, opts[:color_by], opts, [])
|> maybe_encode_field(:y_offset, fn -> opts[:color_by] != nil end, opts[:color_by], opts, [])
|> maybe_flip_axes(flip_axes?)
end

lollipop_opts = [
group_by: [
type: :string,
Expand Down
61 changes: 61 additions & 0 deletions test/tucan_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1703,6 +1703,67 @@ defmodule TucanTest do
end
end

describe "range_bar/4" do
test "with default options" do
data = [
%{category: "A", min: 28, max: 55},
%{category: "B", min: 43, max: 91},
%{category: "C", min: 13, max: 61}
]

expected =
Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar, fill_opacity: 1.0)
|> Vl.encode_field(:y, "category", type: :nominal, axis: [label_angle: 0])
|> Vl.encode_field(:x, "min", type: :quantitative)
|> Vl.encode_field(:x2, "max", type: :quantitative)

assert Tucan.range_bar(data, "category", "min", "max") == expected
end

test "with orient flag set" do
data = [
%{category: "A", min: 28, max: 55},
%{category: "B", min: 43, max: 91},
%{category: "C", min: 13, max: 61}
]

expected =
Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar, fill_opacity: 1.0)
|> Vl.encode_field(:x, "category", type: :nominal, axis: [label_angle: 0])
|> Vl.encode_field(:y, "min", type: :quantitative)
|> Vl.encode_field(:y2, "max", type: :quantitative)

assert Tucan.range_bar(data, "category", "min", "max", orient: :vertical) == expected
end

test "with color_by set and custom options" do
data = [
%{category: "A", min: 28, max: 55},
%{category: "B", min: 43, max: 91},
%{category: "C", min: 13, max: 61}
]

expected =
Vl.new()
|> Vl.data_from_values(data)
|> Vl.mark(:bar, fill_opacity: 1.0, color: "red")
|> Vl.encode_field(:y, "category", type: :nominal, axis: [label_angle: 0])
|> Vl.encode_field(:x, "min", type: :quantitative)
|> Vl.encode_field(:x2, "max", type: :quantitative)
|> Vl.encode_field(:y_offset, "category")
|> Vl.encode_field(:color, "category")

assert Tucan.range_bar(data, "category", "min", "max",
color_by: "category",
fill_color: "red"
) == expected
end
end

describe "lollipop/4" do
test "with default options" do
data = [
Expand Down

0 comments on commit a8e6dee

Please sign in to comment.