diff --git a/README.md b/README.md index 015a4c0..7e6383c 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ Elixir bindings to [Vega-Lite](https://vega.github.io/vega-lite). You can use it inside Livebook to plot graphics or in regular Elixir projects to save graphics to PNG, SVG, PDF, or render them using a -webviewer. +webviewer. [See the documentation](https://hexdocs.pm/vega_lite). -[See the documentation](https://hexdocs.pm/vega_lite). +For a higher-level plotting library, built on top of these bindings, +see [Tucan](https://hexdocs.pm/tucan/). ## Installation diff --git a/guides/data.livemd b/guides/data.livemd deleted file mode 100644 index 672d46a..0000000 --- a/guides/data.livemd +++ /dev/null @@ -1,321 +0,0 @@ -# VegaLite Data - -```elixir -Mix.install([ - {:explorer, "~> 0.6.1"}, - {:kino, "~> 0.10.0"}, - {:vega_lite, github: "livebook-dev/vega_lite", override: true}, - {:kino_vega_lite, "~> 0.1.9"} -]) -``` - -## Introduction - -The `VegaLite.Data` module is designed to provide a shorthand API to plot commonly used charts and high-level abstractions for specialized plots. - -The API can be combined with the main `VegaLite` module at any level and at any point, providing flexibility to achieve the same results in a more concise way without compromising expressiveness. - -Throughout this guide, we will look at how to use the API alone, in combination with the `VegaLite` module, and also show some comparisons between all the possible paths to achieve the same plotting results. - -**Limitations**: `VegaLite.Data` relies on internal type inference, and although all options may be overridden, only data that implements the `Table.Reader` protocol is supported. - -For meaningful examples, we will use the *fuels* and *iris* datasets directly from `Explorer`. - -```elixir -alias Explorer.DataFrame, as: DF -alias VegaLite, as: Vl -alias VegaLite.Data - -fuels = Explorer.Datasets.fossil_fuels() -iris = Explorer.Datasets.iris() - -data = [ - %{"category" => "A", "score" => 28}, - %{"category" => "B", "score" => 50}, - %{"category" => "C", "score" => 34}, - %{"category" => "D", "score" => 42}, - %{"category" => "E", "score" => 39} -] -``` - -## Chart - the shorthand api - -`VegaLite.Data.chart/3` and `VegaLite.Data.chart/4` are the shorthand API. We will use these functions to get quick and concise plots. It's shine for plots that don't require a lot of configuration or customization. - -`VegaLite.Data.chart/3` takes 3 arguments: the data, the mark and a list of fields to be encoded while `VegaLite.Data.chart/4` works similarly, but takes a valid `VegaLite` specification as the first argument. - -```elixir -# A simple bar plot -Vl.new() -|> Vl.data_from_values(data) -|> Vl.mark(:bar) -|> Vl.encode_field(:y, "score", type: :quantitative) -|> Vl.encode_field(:x, "category", type: :nominal) -``` - -```elixir -# The same chart with the shorthand api -Data.chart(data, :bar, x: "category", y: "score") -``` - -Plotting a simple chart is a breeze! As we can see from the comparison above, the code becomes much leaner and handleable. However, the API also accepts a list of options for each argument, allowing more complex results. - -```elixir -# A line plot with point: true without the shorthand api -Vl.new() -|> Vl.data_from_values(fuels, only: ["total", "solid_fuel"]) -|> Vl.mark(:line, point: true) -|> Vl.encode_field(:x, "total", type: :quantitative) -|> Vl.encode_field(:y, "solid_fuel", type: :quantitative) -``` - -```elixir -# A line plot with point: true using the shorthand api -Data.chart(fuels, [type: :line, point: true], x: "total", y: "solid_fuel") -``` - -Now let's see a bit of iteroperability between the api and the main module. We'll plot the same line chart but now with a title and a custom width. - -```elixir -# Without the shorthand api -Vl.new(title: "Fuels", width: 400) -|> Vl.data_from_values(fuels, only: ["total", "solid_fuel"]) -|> Vl.mark(:line, point: true) -|> Vl.encode_field(:x, "total", type: :quantitative) -|> Vl.encode_field(:y, "solid_fuel", type: :quantitative) -``` - -```elixir -# With the shorthand api -Vl.new(title: "Fuels", width: 400) -|> Data.chart(fuels, [type: :line, point: true], x: "total", y: "solid_fuel") -``` - -If a channel requires more configuration, the flexibility of the API comes into play. - -```elixir -Vl.new(width: 500, height: 300, title: "Fuels") -|> Vl.data_from_values(fuels, only: ["total", "solid_fuel"]) -|> Vl.mark(:point) -|> Vl.encode_field(:x, "total", type: :quantitative) -|> Vl.encode_field(:y, "solid_fuel", type: :quantitative) -|> Vl.encode_field(:color, "total", type: :quantitative, scale: [scheme: "category10"]) -``` - -In the example above, we have a color channel that requires more customization. While it's possible to get the exact same plot using only the shorthand API, the expressiveness may be sacrificed. It's precisely in these cases that using the API together with the main module will probably result in more readable code. Let's take a look and compare the possible combinations between the API and the `VegaLite` module. - -```elixir -# Using mainly the shorthand api -Vl.new(width: 500, height: 300, title: "Combined") -|> Data.chart(fuels, :point, - x: "total", - y: "solid_fuel", - color: [field: "total", type: :quantitative, scale: [scheme: "category10"]] -) -``` - -```elixir -# Piping the shorthand api into a enconde_field -Vl.new(width: 500, height: 300, title: "Fuels") -|> Data.chart(fuels, :point, x: "total", y: "solid_fuel") -|> Vl.encode_field(:color, "total", type: :quantitative, scale: [scheme: "category10"]) -``` - -As we can see, the API is flexible enough to allow it to be piped from `VegaLite`, piped to `VegaLite` or both! In principle, you are free to choose the code that best suits your needs, ideally aiming for a balance between conciseness and expressiveness. - -`:extra_fields` - By default, the shortened API uses a subset of the used fields from the data. You can override this if necessary, for example, if you want to pipe to an `encode_field` that uses a previously unused field. - -```elixir -# We want to include "year" using the :extra_fields, so that we can use it as a color later on -Vl.new(width: 500, height: 300, title: "Fuels") -|> Data.chart(fuels, :point, x: "total", y: "solid_fuel", extra_fields: ["year"]) -|> Vl.encode_field(:color, "year", type: :nominal, scale: [scheme: "category10"]) -``` - -## Specialized plots - -Specialized plots provide high-level abstractions for commonly used complex charts. - -### Heatmap - -A heatmap shows the values of a key variable of interest on two axes as a grid of colored squares. Although widely used and useful, plotting heatmaps directly from VegaLite requires a lot of code. - -*For a more concrete example, we will use precomputed data from the correlation matrix of the wine dataset.* - - - -```elixir -corr_to_plot = %{ - "corr_val" => [1.0, -0.02, 0.29, 0.09, 0.02, -0.05, 0.09, 0.27, -0.43, -0.02, - -0.12, -0.11, -0.02, 1.0, -0.15, 0.06, 0.07, -0.1, 0.09, 0.03, -0.03, -0.04, - 0.07, -0.19, 0.29, -0.15, 1.0, 0.09, 0.11, 0.09, 0.12, 0.15, -0.16, 0.06, - -0.08, -0.01, 0.09, 0.06, 0.09, 1.0, 0.09, 0.3, 0.4, 0.84, -0.19, -0.03, - -0.45, -0.1, 0.02, 0.07, 0.11, 0.09, 1.0, 0.1, 0.2, 0.26, -0.09, 0.02, -0.36, - -0.21, -0.05, -0.1, 0.09, 0.3, 0.1, 1.0, 0.62, 0.29, 0.0, 0.06, -0.25, 0.01, - 0.09, 0.09, 0.12, 0.4, 0.2, 0.62, 1.0, 0.53, 0.0, 0.13, -0.45, -0.17, 0.27, - 0.03, 0.15, 0.84, 0.26, 0.29, 0.53, 1.0, -0.09, 0.07, -0.78, -0.31, -0.43, - -0.03, -0.16, -0.19, -0.09, 0.0, 0.0, -0.09, 1.0, 0.16, 0.12, 0.1, -0.02, - -0.04, 0.06, -0.03, 0.02, 0.06, 0.13, 0.07, 0.16, 1.0, -0.02, 0.05, -0.12, - 0.07, -0.08, -0.45, -0.36, -0.25, -0.45, -0.78, 0.12, -0.02, 1.0, 0.44, - -0.11, -0.19, -0.01, -0.1, -0.21, 0.01, -0.17, -0.31, 0.1, 0.05, 0.44, 1.0], - "x" => ["fixed acidity", "volatile acidity", "citric acid", "residual sugar", - "chlorides", "free sulfur dioxide", "total sulfur dioxide", "density", "pH", - "sulphates", "alcohol", "quality", "fixed acidity", "volatile acidity", - "citric acid", "residual sugar", "chlorides", "free sulfur dioxide", - "total sulfur dioxide", "density", "pH", "sulphates", "alcohol", "quality", - "fixed acidity", "volatile acidity", "citric acid", "residual sugar", - "chlorides", "free sulfur dioxide", "total sulfur dioxide", "density", "pH", - "sulphates", "alcohol", "quality", "fixed acidity", "volatile acidity", - "citric acid", "residual sugar", "chlorides", "free sulfur dioxide", - "total sulfur dioxide", "density", "pH", "sulphates", "alcohol", "quality", - "fixed acidity", "volatile acidity", "citric acid", "residual sugar", - "chlorides", "free sulfur dioxide", "total sulfur dioxide", "density", "pH", - "sulphates", "alcohol", "quality", "fixed acidity", "volatile acidity", - "citric acid", "residual sugar", "chlorides", "free sulfur dioxide", - "total sulfur dioxide", "density", "pH", "sulphates", "alcohol", "quality", - "fixed acidity", "volatile acidity", "citric acid", "residual sugar", - "chlorides", "free sulfur dioxide", "total sulfur dioxide", "density", "pH", - "sulphates", "alcohol", "quality", "fixed acidity", "volatile acidity", - "citric acid", "residual sugar", "chlorides", "free sulfur dioxide", - "total sulfur dioxide", "density", "pH", "sulphates", "alcohol", "quality", - "fixed acidity", "volatile acidity", "citric acid", "residual sugar", - "chlorides", "free sulfur dioxide", "total sulfur dioxide", "density", "pH", - "sulphates", "alcohol", "quality", "fixed acidity", "volatile acidity", - "citric acid", "residual sugar", "chlorides", "free sulfur dioxide", - "total sulfur dioxide", "density", "pH", "sulphates", "alcohol", "quality", - "fixed acidity", "volatile acidity", "citric acid", "residual sugar", - "chlorides", "free sulfur dioxide", "total sulfur dioxide", "density", "pH", - "sulphates", "alcohol", "quality", "fixed acidity", "volatile acidity", - "citric acid", "residual sugar", "chlorides", "free sulfur dioxide", - "total sulfur dioxide", "density", "pH", "sulphates", "alcohol", "quality"], - "y" => ["fixed acidity", "fixed acidity", "fixed acidity", "fixed acidity", - "fixed acidity", "fixed acidity", "fixed acidity", "fixed acidity", - "fixed acidity", "fixed acidity", "fixed acidity", "fixed acidity", - "volatile acidity", "volatile acidity", "volatile acidity", - "volatile acidity", "volatile acidity", "volatile acidity", - "volatile acidity", "volatile acidity", "volatile acidity", - "volatile acidity", "volatile acidity", "volatile acidity", "citric acid", - "citric acid", "citric acid", "citric acid", "citric acid", "citric acid", - "citric acid", "citric acid", "citric acid", "citric acid", "citric acid", - "citric acid", "residual sugar", "residual sugar", "residual sugar", - "residual sugar", "residual sugar", "residual sugar", "residual sugar", - "residual sugar", "residual sugar", "residual sugar", "residual sugar", - "residual sugar", "chlorides", "chlorides", "chlorides", "chlorides", - "chlorides", "chlorides", "chlorides", "chlorides", "chlorides", "chlorides", - "chlorides", "chlorides", "free sulfur dioxide", "free sulfur dioxide", - "free sulfur dioxide", "free sulfur dioxide", "free sulfur dioxide", - "free sulfur dioxide", "free sulfur dioxide", "free sulfur dioxide", - "free sulfur dioxide", "free sulfur dioxide", "free sulfur dioxide", - "free sulfur dioxide", "total sulfur dioxide", "total sulfur dioxide", - "total sulfur dioxide", "total sulfur dioxide", "total sulfur dioxide", - "total sulfur dioxide", "total sulfur dioxide", "total sulfur dioxide", - "total sulfur dioxide", "total sulfur dioxide", "total sulfur dioxide", - "total sulfur dioxide", "density", "density", "density", "density", - "density", "density", "density", "density", "density", "density", "density", - "density", "pH", "pH", "pH", "pH", "pH", "pH", "pH", "pH", "pH", "pH", "pH", - "pH", "sulphates", "sulphates", "sulphates", "sulphates", "sulphates", - "sulphates", "sulphates", "sulphates", "sulphates", "sulphates", "sulphates", - "sulphates", "alcohol", "alcohol", "alcohol", "alcohol", "alcohol", - "alcohol", "alcohol", "alcohol", "alcohol", "alcohol", "alcohol", "alcohol", - "quality", "quality", "quality", "quality", "quality", "quality", "quality", - "quality", "quality", "quality", "quality", "quality"] -} -|> Explorer.DataFrame.new() -``` - -```elixir -Vl.new(title: "Correlation matrix", width: 600, height: 600) -|> Vl.data_from_values(corr_to_plot, only: ["x", "y", "corr_val"]) -|> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "x", type: :nominal) - |> Vl.encode_field(:y, "y", type: :nominal) - |> Vl.encode_field(:color, "corr_val", type: :quantitative), - Vl.new() - |> Vl.data_from_values(corr_to_plot) - |> Vl.mark(:text) - |> Vl.encode_field(:x, "x", type: :nominal) - |> Vl.encode_field(:y, "y", type: :nominal) - |> Vl.encode_field(:text, "corr_val", type: :quantitative) -]) -``` - -We can use our already explored shorthand API to simplify it. - -```elixir -Vl.new(title: "Correlation matrix", width: 600, height: 600) -|> Vl.layers([ - Data.chart(corr_to_plot, :rect, - x: [field: "x", type: :nominal], - y: [field: "y", type: :nominal], - color: "corr_val" - ), - Data.chart(corr_to_plot, :text, - x: [field: "x", type: :nominal], - y: [field: "y", type: :nominal], - text: "corr_val" - ) -]) -``` - -Or we can go even further and use the `VegaLite.Data.heatmap/2` function alone or the `VegaLite.Data.heatmap/3` function in combination with `VegaLite`. - -The specialized plots follow the same principle as the shorthand API, they can be combined with the main module, and each argument can also take a list of options to override the defaults. - -```elixir -Vl.new(title: "Correlation matrix", width: 600, height: 600) -|> Data.heatmap(corr_to_plot, - x: "x", - y: "y", - color: "corr_val", - text: "corr_val" -) -``` - -### Density heatmap - -A density heatmap is a heatmap of binned quantitative data. It requires binned data and an aggregation function. - - - -To plot them, we can use the `VegaLite.Data.density_heatmap/2` and `VegaLite.Data.density_heatmap/3` functions. They are very similar to `VegaLite.Data.heatmap/2` and `VegaLite.Data.heatmap/3`, but expect quantitative data. - -The default agregation functions is `:count`. - -```elixir -Vl.new(title: "Density heatmap", width: 400, height: 250) -|> Data.density_heatmap(iris, - x: "sepal_length", - y: "sepal_width", - color: "sepal_length", - text: "sepal_width" -) -``` - -### Jointplot - -A jointplot shows the relationship between two variables and the distribution of individuals of each one. It expects quantitative data. - -```elixir -Vl.new(title: "Jointplot") -|> Data.joint_plot(iris, :circle, x: "sepal_length", y: "sepal_width", color: "sepal_length") -``` - -The `VegaLite.Data.joint_plot/4` function takes care of all the visual aspects, such as adjusting the size of marginal histograms and their spacing. - -The second argument receives the `:mark` for the main chart and its options. It supports `:density_heatmap` as a special value. - -Keep in mind that all customizations apply to the main chart only. The marginal histograms are not customizable. - -```elixir -Vl.new(title: "Jointplot with a density heatmap", width: 500, height: 350) -|> Data.joint_plot( - iris, - :density_heatmap, - x: "sepal_length", - y: "sepal_width", - color: "sepal_length", - text: "sepal_length" -) -``` diff --git a/lib/vega_lite/data.ex b/lib/vega_lite/data.ex index 29de470..5d000ad 100644 --- a/lib/vega_lite/data.ex +++ b/lib/vega_lite/data.ex @@ -1,10 +1,6 @@ defmodule VegaLite.Data do @moduledoc """ - Data is a VegaLite module designed to provide a shorthand API for commonly used charts and - high-level abstractions for specialized plots. - - Optionally accepts and always returns a valid `VegaLite` spec, fostering flexibility to be used - alone or in combination with the `VegaLite` module at any level and at any point. + Data is a VegaLite module designed to provide a shorthand API for charts based on data. It relies on internal type inference, and although all options can be overridden, only data that implements the `Table.Reader` protocol is supported. @@ -58,17 +54,14 @@ defmodule VegaLite.Data do """ @spec chart(VegaLite.t(), Table.Reader.t(), keyword()) :: VegaLite.t() def chart(%Vl{} = vl, data, fields) do - chart_no_data(vl, data, fields) + vl + |> encode_fields(normalize_fields(fields), columns_for(data)) |> attach_data(data, fields) end @spec chart(Table.Reader.t(), atom() | keyword(), keyword()) :: VegaLite.t() def chart(data, mark, fields), do: chart(Vl.new(), data, mark, fields) - defp chart_no_data(vl, data, fields) do - encode_fields(vl, normalize_fields(fields), columns_for(data)) - end - @doc """ Same as chart/3 but receives a valid `VegaLite` specification as a first argument. @@ -96,196 +89,12 @@ defmodule VegaLite.Data do """ @spec chart(VegaLite.t(), Table.Reader.t(), atom() | keyword(), keyword()) :: VegaLite.t() def chart(vl, data, mark, fields) do - chart_no_data(vl, data, mark, fields) - |> attach_data(data, fields) - end - - defp chart_no_data(vl, data, mark, fields) do vl |> encode_mark(mark) |> encode_fields(normalize_fields(fields), columns_for(data)) - end - - @doc """ - Returns the specification of a heat map for a given data and a list of fields to be encoded. - - As a specialized chart, the heatmap expects an `:x` and `:y` and optionally a `:color`, a - `:text` and a `:text_color` fields. Defaults to `:nominal` for the axes and `:quantitative` - for color and text if types are not specified. - - ## Examples - - data = [ - %{"category" => "A", "score" => 28}, - %{"category" => "B", "score" => 55} - ] - - Data.heatmap(data, x: "category", y: "score", color: "score", text: "category") - - With an existing VegaLite spec: - - Vl.new(title: "Heatmap", width: 500) - |> Data.heatmap(data, x: "category", y: "score", color: "score", text: "category") - """ - @spec heatmap(VegaLite.t(), Table.Reader.t(), keyword()) :: VegaLite.t() - def heatmap(vl \\ Vl.new(), data, fields) do - for key <- [:x, :y], is_nil(fields[key]) do - raise ArgumentError, "the #{key} field is required to plot a heatmap" - end - - heatmap_no_data(vl, data, fields, &heatmap_defaults/2) - |> attach_data(data, fields) - end - - defp heatmap_no_data(vl, data, fields, fun) do - cols = columns_for(data) - fields = normalize_fields(fields, fun) - text_fields = Keyword.take(fields, [:text, :text_color, :x, :y]) - rect_fields = Keyword.drop(fields, [:text, :text_color]) - - {text_color, text_fields} = Keyword.pop(text_fields, :text_color) - - text_fields = - if text_color, do: Keyword.put_new(text_fields, :color, text_color), else: text_fields - - text_layer = if fields[:text], do: [encode_layer(cols, :text, text_fields)], else: [] - rect_layer = [encode_layer(cols, :rect, rect_fields)] - - Vl.layers(vl, rect_layer ++ text_layer) - end - - defp heatmap_defaults(field, opts) when field in [:x, :y] do - Keyword.put_new(opts, :type, :nominal) - end - - defp heatmap_defaults(field, opts) when field in [:color, :text] do - Keyword.put_new(opts, :type, :quantitative) - end - - defp heatmap_defaults(_field, opts), do: opts - - @doc """ - Returns the specification of a density heat map for a given data and a list of fields to be encoded. - - As a specialized chart, the density heatmap expects the `:x` and `:y` axes, a `:color` field and - optionally a `:text` and a `:text_color` fields. All data must be `:quantitative` and the default - aggregation function is `:count`. - - ## Examples - - data = [ - %{"total_bill" => 16.99, "tip" => 1.0}, - %{"total_bill" => 10.34, "tip" => 1.66} - ] - - Data.density_heatmap(data, x: "total_bill", y: "tip", color: "total_bill", text: "tip") - - With an existing VegaLite spec: - - Vl.new(title: "Density Heatmap", width: 500) - |> Data.heatmap(data, x: "total_bill", y: "tip", color: "total_bill", text: "tip") - """ - @spec density_heatmap(VegaLite.t(), Table.Reader.t(), keyword()) :: VegaLite.t() - def density_heatmap(vl \\ Vl.new(), data, fields) do - for key <- [:x, :y, :color], is_nil(fields[key]) do - raise ArgumentError, "the #{key} field is required to plot a density heatmap" - end - - heatmap_no_data(vl, data, fields, &density_heatmap_defaults/2) |> attach_data(data, fields) end - defp density_heatmap_defaults(field, opts) when field in [:x, :y] do - opts |> Keyword.put_new(:type, :quantitative) |> Keyword.put_new(:bin, true) - end - - defp density_heatmap_defaults(field, opts) when field in [:color, :text] do - opts |> Keyword.put_new(:type, :quantitative) |> Keyword.put_new(:aggregate, :count) - end - - defp density_heatmap_defaults(_field, opts), do: opts - - @doc """ - Returns the specification of a joint plot with marginal histograms for a given data and a - list of fields to be encoded. - - As a specialized chart, the jointplot expects an `:x` and `:y` and optionally a `:color` and a - `:text` field. All data must be `:quantitative`. - - Besides all marks, it supports `:density_heatmap` as a special value. - - All customizations apply to the main chart only. The marginal histograms are not customizable. - - ## Examples - - data = [ - %{"total_bill" => 16.99, "tip" => 1.0}, - %{"total_bill" => 10.34, "tip" => 1.66} - ] - - Data.joint_plot(data, :bar, x: "total_bill", y: "tip") - - With an existing VegaLite spec: - - Vl.new(title: "Joint Plot", width: 500) - |> Data.joint_plot(data, :bar, x: "total_bill", y: "tip", color: "total_bill") - """ - @spec joint_plot(VegaLite.t(), Table.Reader.t(), atom() | keyword(), keyword()) :: VegaLite.t() - def joint_plot(vl \\ Vl.new(), data, mark, fields) do - for key <- [:x, :y], is_nil(fields[key]) do - raise ArgumentError, "the #{key} field is required to plot a jointplot" - end - - root_opts = - Enum.filter([width: vl.spec["width"], height: vl.spec["height"]], fn {_k, v} -> v end) - - main_chart = build_main_jointplot(Vl.new(root_opts), data, mark, fields) - {x_hist, y_hist} = build_marginal_jointplot(normalize_fields(fields), root_opts) - - vl - |> Map.update!(:spec, &Map.merge(&1, %{"bounds" => "flush", "spacing" => 15})) - |> attach_data(data, fields) - |> Vl.concat( - [x_hist, Vl.new(spacing: 15, bounds: :flush) |> Vl.concat([main_chart, y_hist])], - :vertical - ) - end - - defp build_main_jointplot(vl, data, :density_heatmap, fields) do - heatmap_no_data(vl, data, fields, &density_heatmap_defaults/2) - end - - defp build_main_jointplot(vl, data, mark, fields) do - chart_no_data(vl, data, mark, fields) - end - - defp build_marginal_jointplot(fields, opts) do - {x, y} = {fields[:x][:field], fields[:y][:field]} - - xx = [type: :quantitative, bin: true, axis: nil] - xy = [type: :quantitative, aggregate: :count, title: ""] - - x_root = - if opts[:width], do: Vl.new(height: 60, width: opts[:width]), else: Vl.new(height: 60) - - y_root = - if opts[:height], do: Vl.new(width: 60, height: opts[:height]), else: Vl.new(width: 60) - - x_hist = - x_root - |> Vl.mark(:bar) - |> Vl.encode_field(:x, x, xx) - |> Vl.encode_field(:y, x, xy) - - y_hist = - y_root - |> Vl.mark(:bar) - |> Vl.encode_field(:x, y, xy) - |> Vl.encode_field(:y, y, xx) - - {x_hist, y_hist} - end - ## Shared helpers defp encode_mark(vl, opts) when is_list(opts) do @@ -303,10 +112,6 @@ defmodule VegaLite.Data do end) end - defp encode_layer(cols, mark, fields) do - Vl.new() |> encode_mark(mark) |> encode_fields(fields, cols) - end - defp attach_data(vl, data, fields) do used_fields = fields diff --git a/mix.exs b/mix.exs index b0a0faf..fd31504 100644 --- a/mix.exs +++ b/mix.exs @@ -35,8 +35,7 @@ defmodule VegaLite.MixProject do [ main: "VegaLite", source_url: "https://github.com/livebook-dev/vega_lite", - source_ref: "v#{@version}", - extras: ["guides/data.livemd"] + source_ref: "v#{@version}" ] end diff --git a/test/vega_lite/data_test.exs b/test/vega_lite/data_test.exs index 363e77a..e1869d9 100644 --- a/test/vega_lite/data_test.exs +++ b/test/vega_lite/data_test.exs @@ -213,748 +213,4 @@ defmodule VegaLite.DataTest do assert vl == sh end end - - describe "heatmap" do - test "simple" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - ]) - - assert vl == Data.heatmap(@data, x: "height", y: "weight") - end - - test "with color" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:color, "height", type: :quantitative) - ]) - - assert vl == Data.heatmap(@data, x: "height", y: "weight", color: "height") - end - - test "with text and color" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:color, "height", type: :quantitative), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:text, "height", type: :quantitative) - ]) - - assert vl == Data.heatmap(@data, x: "height", y: "weight", color: "height", text: "height") - end - - test "with text_color" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:color, "height", type: :quantitative), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:text, "height", type: :quantitative) - |> Vl.encode_field(:color, "height", type: :quantitative) - ]) - - assert vl == - Data.heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "height", - text_color: "height" - ) - end - - test "with text_color with condition" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:color, "height", type: :quantitative), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:text, "height", type: :quantitative) - |> Vl.encode_field(:color, "height", - type: :quantitative, - condition: [ - [test: "datum['height'] < 0", value: :white], - [test: "datum['height'] >= 0", value: :black] - ] - ) - ]) - - assert vl == - Data.heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "height", - text_color: [ - field: "height", - condition: [ - [test: "datum['height'] < 0", value: :white], - [test: "datum['height'] >= 0", value: :black] - ] - ] - ) - end - - test "with title and extra fields" do - vl = - Vl.new(title: "Heatmap") - |> Vl.data_from_values(@data, only: ["height", "weight", "width"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:color, "height", type: :quantitative), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:text, "height", type: :quantitative) - ]) - - assert vl == - Vl.new(title: "Heatmap") - |> Data.heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "height", - extra_fields: ["width"] - ) - end - - test "with specified types" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative) - |> Vl.encode_field(:color, "height", type: :nominal), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative) - |> Vl.encode_field(:text, "height", type: :quantitative) - ]) - - assert vl == - Data.heatmap(@data, - x: [field: "height", type: :quantitative], - y: [field: "weight", type: :quantitative], - color: [field: "height", type: :nominal], - text: "height" - ) - end - - test "with a text field different from the axes" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight", "width"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:color, "height", type: :quantitative), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :nominal) - |> Vl.encode_field(:y, "weight", type: :nominal) - |> Vl.encode_field(:text, "width", type: :quantitative) - ]) - - assert vl == Data.heatmap(@data, x: "height", y: "weight", color: "height", text: "width") - end - - test "raises an error when the x field is not given" do - assert_raise ArgumentError, "the x field is required to plot a heatmap", fn -> - Data.heatmap(@data, y: "y") - end - end - - test "raises an error when the y field is not given" do - assert_raise ArgumentError, "the y field is required to plot a heatmap", fn -> - Data.heatmap(@data, x: "x", text: "text") - end - end - end - - describe "density heatmap" do - test "simple density heatmap" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count) - ]) - - assert vl == Data.density_heatmap(@data, x: "height", y: "weight", color: "height") - end - - test "with title" do - vl = - Vl.new(title: "Density heatmap") - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count) - ]) - - assert vl == - Vl.new(title: "Density heatmap") - |> Data.density_heatmap(@data, x: "height", y: "weight", color: "height") - end - - test "with specified bins" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: [maxbins: 10]) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: [maxbins: 10]) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count) - ]) - - assert vl == - Data.density_heatmap(@data, - x: [field: "height", bin: [maxbins: 10]], - y: [field: "weight", bin: [maxbins: 10]], - color: "height" - ) - end - - test "with specified aggregate for color" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :mean) - ]) - - assert vl == - Data.density_heatmap(@data, - x: "height", - y: "weight", - color: [field: "height", aggregate: :mean] - ) - end - - test "with text" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:text, "height", type: :quantitative, aggregate: :count) - ]) - - assert vl == - Data.density_heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "height" - ) - end - - test "with text_color" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:text, "height", type: :quantitative, aggregate: :count) - |> Vl.encode_field(:color, "height", type: :quantitative) - ]) - - assert vl == - Data.density_heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "height", - text_color: "height" - ) - end - - test "with text_color with condition" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:text, "height", type: :quantitative, aggregate: :count) - |> Vl.encode_field(:color, "height", - type: :quantitative, - condition: [ - [test: "datum['height'] < 0", value: :white], - [test: "datum['height'] >= 0", value: :black] - ] - ) - ]) - - assert vl == - Data.density_heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "height", - text_color: [ - field: "height", - condition: [ - [test: "datum['height'] < 0", value: :white], - [test: "datum['height'] >= 0", value: :black] - ] - ] - ) - end - - test "with specified aggregate for text" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:text, "height", type: :quantitative, aggregate: :mean) - ]) - - assert vl == - Data.density_heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: [field: "height", aggregate: :mean] - ) - end - - test "with text different from the axes" do - vl = - Vl.new() - |> Vl.data_from_values(@data, only: ["height", "weight", "width"]) - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:text, "width", type: :quantitative, aggregate: :count) - ]) - - assert vl == - Data.density_heatmap(@data, - x: "height", - y: "weight", - color: "height", - text: "width" - ) - end - - test "raises an error when the x field is not given" do - assert_raise ArgumentError, "the x field is required to plot a density heatmap", fn -> - Data.density_heatmap(@data, y: "y") - end - end - - test "raises an error when the y field is not given" do - assert_raise ArgumentError, "the y field is required to plot a density heatmap", fn -> - Data.density_heatmap(@data, x: "x", text: "text") - end - end - - test "raises an error when the color field is not given" do - assert_raise ArgumentError, "the color field is required to plot a density heatmap", fn -> - Data.density_heatmap(@data, x: "x", y: "y") - end - end - end - - describe "jointplot" do - test "simple jointplot" do - vl = - Vl.new(spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new() - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == Data.joint_plot(@data, :circle, x: "height", y: "weight") - end - - test "with title" do - vl = - Vl.new(title: "Jointplot", spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new() - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == - Vl.new(title: "Jointplot") - |> Data.joint_plot(@data, :circle, x: "height", y: "weight") - end - - test "with custom width" do - vl = - Vl.new(title: "Jointplot", width: 500, spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60, width: 500) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new(width: 500) - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == - Vl.new(title: "Jointplot", width: 500) - |> Data.joint_plot(@data, :circle, x: "height", y: "weight") - end - - test "with custom height" do - vl = - Vl.new(title: "Jointplot", height: 350, spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new(height: 350) - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative), - Vl.new(width: 60, height: 350) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == - Vl.new(title: "Jointplot", height: 350) - |> Data.joint_plot(@data, :circle, x: "height", y: "weight") - end - - test "with custom width and height" do - vl = - Vl.new(title: "Jointplot", width: 500, height: 350, spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60, width: 500) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new(width: 500, height: 350) - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative), - Vl.new(width: 60, height: 350) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == - Vl.new(title: "Jointplot", width: 500, height: 350) - |> Data.joint_plot(@data, :circle, x: "height", y: "weight") - end - - test "with color" do - vl = - Vl.new(spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight", "width"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new() - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative) - |> Vl.encode_field(:color, "width", type: :quantitative), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == Data.joint_plot(@data, :circle, x: "height", y: "weight", color: "width") - end - - test "with text" do - vl = - Vl.new(spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight", "width"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new() - |> Vl.mark(:circle) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative) - |> Vl.encode_field(:text, "width", type: :quantitative), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == Data.joint_plot(@data, :circle, x: "height", y: "weight", text: "width") - end - - test "mark with options" do - vl = - Vl.new(spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", type: :quantitative, aggregate: :count, title: ""), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new() - |> Vl.mark(:point, filled: true) - |> Vl.encode_field(:x, "height", type: :quantitative) - |> Vl.encode_field(:y, "weight", type: :quantitative), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", type: :quantitative, aggregate: :count, title: "") - ]) - ], - :vertical - ) - - assert vl == Data.joint_plot(@data, [type: :point, filled: true], x: "height", y: "weight") - end - - test "with a supported specialized as mark" do - vl = - Vl.new(spacing: 15, bounds: :flush) - |> Vl.data_from_values(@data, only: ["height", "weight"]) - |> Vl.concat( - [ - Vl.new(height: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:y, "height", - type: :quantitative, - aggregate: :count, - title: "" - ), - Vl.new(spacing: 15, bounds: :flush) - |> Vl.concat([ - Vl.new() - |> Vl.layers([ - Vl.new() - |> Vl.mark(:rect) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:color, "height", type: :quantitative, aggregate: :count), - Vl.new() - |> Vl.mark(:text) - |> Vl.encode_field(:x, "height", type: :quantitative, bin: true) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true) - |> Vl.encode_field(:text, "height", type: :quantitative, aggregate: :count) - ]), - Vl.new(width: 60) - |> Vl.mark(:bar) - |> Vl.encode_field(:y, "weight", type: :quantitative, bin: true, axis: nil) - |> Vl.encode_field(:x, "weight", - type: :quantitative, - aggregate: :count, - title: "" - ) - ]) - ], - :vertical - ) - - assert vl == - Data.joint_plot( - @data, - :density_heatmap, - x: "height", - y: "weight", - color: "height", - text: "height" - ) - end - - test "raises an error when the x field is not given" do - assert_raise ArgumentError, "the x field is required to plot a jointplot", fn -> - Data.joint_plot(@data, :point, y: "y") - end - end - - test "raises an error when the y field is not given" do - assert_raise ArgumentError, "the y field is required to plot a jointplot", fn -> - Data.joint_plot(@data, :bar, x: "x", text: "text") - end - end - end end