├── .credo.exs
├── .formatter.exs
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── ROADMAP.md
├── guides
├── annotate.md
├── assets
│ ├── annotate_1.svg
│ ├── annotate_2.svg
│ ├── annotate_3.svg
│ ├── geom_bar_1.svg
│ ├── geom_bar_2.svg
│ ├── geom_boxplot_1.svg
│ ├── geom_boxplot_2.svg
│ ├── geom_boxplot_3.svg
│ ├── geom_boxplot_4.svg
│ ├── geom_line_1.svg
│ ├── geom_line_2.svg
│ ├── geom_line_3.svg
│ ├── geom_point_1.svg
│ ├── geom_point_2.svg
│ ├── geom_point_3.svg
│ ├── geom_point_4.svg
│ ├── geom_point_5.svg
│ ├── geom_point_6.svg
│ ├── geom_point_custom.svg
│ ├── geom_text_1.svg
│ ├── geom_text_2.svg
│ ├── geom_text_3.svg
│ ├── geom_text_4.svg
│ ├── geom_text_5.svg
│ ├── geom_text_6.svg
│ ├── geom_text_7.svg
│ ├── geom_text_8.svg
│ ├── geom_text_9.svg
│ ├── scale_color_viridis_1.svg
│ ├── scale_color_viridis_2.svg
│ ├── scale_color_viridis_3.svg
│ ├── theme_1.svg
│ ├── theme_10.svg
│ ├── theme_11.svg
│ ├── theme_12.svg
│ ├── theme_13.svg
│ ├── theme_2.svg
│ ├── theme_3.svg
│ ├── theme_4.svg
│ ├── theme_5.svg
│ ├── theme_6.svg
│ ├── theme_7.svg
│ ├── theme_8.svg
│ └── theme_9.svg
├── geom_bar.md
├── geom_boxplot.md
├── geom_line.md
├── geom_point.md
├── geom_text.md
├── scale_color_viridis.md
└── theme.md
├── lib
├── ggity.ex
├── ggity
│ ├── annotate.ex
│ ├── axis.ex
│ ├── color.ex
│ ├── draw.ex
│ ├── element.ex
│ ├── element
│ │ ├── line.ex
│ │ ├── rect.ex
│ │ └── text.ex
│ ├── geom
│ │ ├── bar.ex
│ │ ├── blank.ex
│ │ ├── boxplot.ex
│ │ ├── line.ex
│ │ ├── point.ex
│ │ ├── rect.ex
│ │ ├── ribbon.ex
│ │ ├── segment.ex
│ │ └── text.ex
│ ├── html.ex
│ ├── labels.ex
│ ├── layer.ex
│ ├── legend.ex
│ ├── plot.ex
│ ├── scale.ex
│ ├── scale
│ │ ├── alpha_continuous.ex
│ │ ├── alpha_discrete.ex
│ │ ├── alpha_manual.ex
│ │ ├── color_manual.ex
│ │ ├── color_viridis.ex
│ │ ├── continuous.ex
│ │ ├── discrete.ex
│ │ ├── fill_viridis.ex
│ │ ├── identity.ex
│ │ ├── linetype_discrete.ex
│ │ ├── linetype_manual.ex
│ │ ├── shape.ex
│ │ ├── shape_manual.ex
│ │ ├── size.ex
│ │ ├── size_manual.ex
│ │ ├── x_continuous.ex
│ │ ├── x_date.ex
│ │ ├── x_date_time.ex
│ │ ├── x_discrete.ex
│ │ └── y_continuous.ex
│ ├── shapes.ex
│ ├── stat.ex
│ └── theme.ex
└── mix
│ └── tasks
│ ├── doc_examples
│ ├── annotate.ex
│ ├── geom_bar.ex
│ ├── geom_boxplot.ex
│ ├── geom_line.ex
│ ├── geom_point.ex
│ ├── geom_text.ex
│ ├── scale_color_viridis.ex
│ └── theme.ex
│ ├── docs.ex
│ └── examples.ex
├── livebooks
└── geom_point.livemd
├── mix.exs
├── mix.lock
├── priv
├── diamonds.csv
├── economics.csv
├── economics_long.csv
├── mpg.csv
└── tx_housing.csv
└── test
├── ggity_draw_test.exs
├── ggity_geom_line_test.exs
├── ggity_geom_point_test.exs
├── ggity_labels_test.exs
├── ggity_plot_test.exs
├── ggity_theme_test.exs
├── scale
├── ggity_scale_alpha_continuous_test.exs
├── ggity_scale_alpha_discrete_test.exs
├── ggity_scale_alpha_manual_test.exs
├── ggity_scale_color_manual_test.exs
├── ggity_scale_color_viridis_test.exs
├── ggity_scale_identity_test.exs
├── ggity_scale_linetype_discrete_test.exs
├── ggity_scale_linetype_manual_test.exs
├── ggity_scale_shape_manual_test.exs
├── ggity_scale_shape_test.exs
├── ggity_scale_size_manual_test.exs
├── ggity_scale_size_test.exs
├── ggity_scale_x_continuous_test.exs
├── ggity_scale_x_date_test.exs
├── ggity_scale_x_datetime_test.exs
└── ggity_scale_y_continuous_test.exs
├── test_helper.exs
└── visual
├── ggity_geom_bar_test.livemd
├── ggity_geom_boxplot_test.livemd
├── ggity_geom_line_test.livemd
├── ggity_geom_point_test.livemd
├── ggity_geom_ribbon_test.livemd
├── ggity_geom_text_test.livemd
├── ggity_labels_test.livemd
├── ggity_layers_test.livemd
└── ggity_scale_color_viridis_test.livemd
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "mix" # See documentation for possible values
4 | directory: "/" # Location of package manifests
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - master
7 | jobs:
8 | test:
9 | runs-on: ubuntu-20.04
10 | env:
11 | MIX_ENV: test
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | include:
16 | - pair:
17 | elixir: '1.14'
18 | otp: 24.3
19 | - pair:
20 | elixir: '1.14'
21 | otp: 25
22 | - pair:
23 | elixir: '1.15'
24 | otp: 24.3
25 | - pair:
26 | elixir: '1.15'
27 | otp: 25
28 | lint: lint
29 | steps:
30 | - uses: actions/checkout@v2
31 |
32 | - uses: erlef/setup-beam@v1
33 | with:
34 | otp-version: ${{matrix.pair.otp}}
35 | elixir-version: ${{matrix.pair.elixir}}
36 |
37 | - uses: actions/cache@v2
38 | with:
39 | path: deps
40 | key: mix-deps-${{ hashFiles('**/mix.lock') }}
41 |
42 | - run: mix deps.get
43 |
44 | - run: mix format --check-formatted
45 | if: ${{ matrix.lint }}
46 |
47 | - run: mix deps.unlock --check-unused
48 | if: ${{ matrix.lint }}
49 |
50 | - run: mix deps.compile
51 |
52 | - run: mix compile --warnings-as-errors
53 | if: ${{ matrix.lint }}
54 |
55 | - run: mix test
56 | if: ${{ ! matrix.lint }}
57 |
58 | - run: mix test --warnings-as-errors
59 | if: ${{ matrix.lint }}
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | ggity-*.tar
24 |
25 | .DS_Store
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present Steve Rowley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | ## v0.6
4 | * Histograms
5 | * Faceting
6 |
7 | ## Later
8 | * Continuous color scale (including legends)
9 | * `coord_flip` (support horizontal bar charts)
10 | * Implement a method for rendering new points and updating scales only (LiveView optimization)
11 | * Draw legend on any side of the plot (top/left/bottom/right)
12 | * Implement x_lim/y_lim
13 | * ggplot2-ify gridlines approach (breaks, minor breaks)
14 | * `stat_smooth`
15 | * Density plots/stats
--------------------------------------------------------------------------------
/guides/annotate.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.mtcars()
3 | |> Plot.new(%{x: :wt, y: :mpg})
4 | |> Plot.geom_point()
5 | |> Plot.annotate(:text, x: 4, y: 25, label: "Some text")
6 | |> Plot.plot()
7 |
8 | ```
9 | 
10 | ```
11 | Examples.mtcars()
12 | |> Plot.new(%{x: :wt, y: :mpg})
13 | |> Plot.geom_point()
14 | |> Plot.annotate(:rect, xmin: 3, xmax: 4.2, ymin: 12, ymax: 21, alpha: 0.2)
15 | |> Plot.plot()
16 |
17 | ```
18 | 
19 | ```
20 | Examples.mtcars()
21 | |> Plot.new(%{x: :wt, y: :mpg})
22 | |> Plot.geom_point()
23 | |> Plot.annotate(:segment, x: 2.5, xend: 4, y: 15, yend: 25, color: "blue")
24 | |> Plot.plot()
25 |
26 | ```
27 | 
28 |
--------------------------------------------------------------------------------
/guides/assets/geom_text_7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/guides/assets/geom_text_8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/guides/assets/geom_text_9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/guides/geom_bar.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.mpg()
3 | |> Plot.new(%{x: "class"})
4 | |> Plot.geom_bar()
5 | |> Plot.plot()
6 |
7 | ```
8 | 
9 | ```
10 | Examples.mpg()
11 | |> Plot.new(%{x: "class"})
12 | |> Plot.geom_bar(%{fill: "drv"})
13 | |> Plot.plot()
14 |
15 | ```
16 | 
17 |
--------------------------------------------------------------------------------
/guides/geom_boxplot.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.mpg()
3 | |> Plot.new(%{x: "class", y: "hwy"})
4 | |> Plot.geom_boxplot()
5 | |> Plot.plot()
6 |
7 | ```
8 | 
9 | ```
10 | Examples.mpg()
11 | |> Plot.new(%{x: "class", y: "hwy"})
12 | |> Plot.geom_boxplot(fill: "white", color: "#3366FF")
13 | |> Plot.plot()
14 |
15 | ```
16 | 
17 | ```
18 | Examples.mpg()
19 | |> Plot.new(%{x: "class", y: "hwy"})
20 | |> Plot.geom_boxplot(outlier_color: "red")
21 | |> Plot.plot()
22 |
23 | ```
24 | 
25 | ```
26 | Examples.mpg()
27 | |> Plot.new(%{x: "class", y: "hwy"})
28 | |> Plot.geom_boxplot(%{color: "drv"})
29 | |> Plot.plot()
30 |
31 | ```
32 | 
33 |
--------------------------------------------------------------------------------
/guides/geom_line.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.economics()
3 | |> Plot.new(%{x: "date", y: "unemploy"})
4 | |> Plot.geom_line()
5 | |> Plot.plot()
6 |
7 | ```
8 | 
9 | ```
10 | Examples.economics_long()
11 | |> Plot.new(%{x: "date", y: "value01", color: "variable"})
12 | |> Plot.geom_line()
13 | |> Plot.plot()
14 |
15 | ```
16 | 
17 | ```
18 | Examples.economics()
19 | |> Plot.new(%{x: "date", y: "unemploy"})
20 | |> Plot.geom_line(color: "red")
21 | |> Plot.plot()
22 |
23 | ```
24 | 
25 |
--------------------------------------------------------------------------------
/guides/geom_point.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.mtcars()
3 | |> Plot.new(%{x: :wt, y: :mpg})
4 | |> Plot.geom_point()
5 | |> Plot.plot()
6 |
7 | ```
8 | 
9 | ```
10 | # Add aesthetic mapping to color
11 | Examples.mtcars()
12 | |> Plot.new(%{x: :wt, y: :mpg})
13 | |> Plot.geom_point(%{color: :cyl})
14 | |> Plot.plot()
15 |
16 | ```
17 | 
18 | ```
19 | # Add aesthetic mapping to shape
20 | Examples.mtcars()
21 | |> Plot.new(%{x: :wt, y: :mpg})
22 | |> Plot.geom_point(%{shape: :cyl})
23 | |> Plot.plot()
24 |
25 | ```
26 | 
27 | ```
28 | # Add aesthetic mapping to size (for circles, a bubble chart)
29 | Examples.mtcars()
30 | |> Plot.new(%{x: :wt, y: :mpg})
31 | |> Plot.geom_point(%{size: :qsec})
32 | |> Plot.plot()
33 |
34 | ```
35 | 
36 | ```
37 | # Set aesthetics to fixed value
38 | Examples.mtcars()
39 | |> Plot.new(%{x: :wt, y: :mpg})
40 | |> Plot.geom_point(color: "red", size: 5)
41 | |> Plot.plot()
42 |
43 | ```
44 | 
45 |
--------------------------------------------------------------------------------
/guides/geom_text.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.mtcars()
3 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
4 | |> Plot.geom_text()
5 | |> Plot.plot()
6 |
7 | ```
8 | 
9 | ```
10 | # Set the font size for the label
11 | Examples.mtcars()
12 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
13 | |> Plot.geom_text(size: 10)
14 | |> Plot.plot()
15 |
16 | ```
17 | 
18 | ```
19 | # Shift positioning
20 | Examples.mtcars()
21 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
22 | |> Plot.geom_point(size: 2)
23 | |> Plot.geom_text(size: 5, hjust: :left, nudge_x: 3)
24 | |> Plot.plot()
25 |
26 | ```
27 | 
28 | ```
29 | Examples.mtcars()
30 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
31 | |> Plot.geom_point(size: 2)
32 | |> Plot.geom_text(size: 5, vjust: :top, nudge_y: 3)
33 | |> Plot.plot()
34 |
35 | ```
36 | 
37 | ```
38 | # Map other aesthetics
39 | Examples.mtcars()
40 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
41 | |> Plot.geom_text(%{color: :cyl}, size: 5)
42 | |> Plot.plot()
43 |
44 | ```
45 | 
46 | ```
47 | # Add a text annotation
48 | Examples.mtcars()
49 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
50 | |> Plot.geom_text(size: 5)
51 | |> Plot.annotate(:text, label: "plot mpg vs. wt", x: 1.5, y: 15, size: 8, color: "red")
52 | |> Plot.plot()
53 |
54 | ```
55 | 
56 | ```
57 | # Bar chart labelling
58 | [%{x: "1", y: 1, grp: "a"},
59 | %{x: "1", y: 3, grp: "b"},
60 | %{x: "2", y: 2, grp: "a"},
61 | %{x: "2", y: 1, grp: "b"},]
62 | |> Plot.new(%{x: "x", y: "y", group: "grp"})
63 | |> Plot.geom_col(%{fill: "grp"}, position: :dodge)
64 | |> Plot.geom_text(%{label: "y"}, position: :dodge, size: 6)
65 | |> Plot.plot()
66 |
67 | ```
68 | 
69 | ```
70 | # Nudge the label up a bit
71 | [%{x: "1", y: 1, grp: "a"},
72 | %{x: "1", y: 3, grp: "b"},
73 | %{x: "2", y: 2, grp: "a"},
74 | %{x: "2", y: 1, grp: "b"},]
75 | |> Plot.new(%{x: "x", y: "y", group: "grp"})
76 | |> Plot.geom_col(%{fill: "grp"}, position: :dodge)
77 | |> Plot.geom_text(%{label: "y"}, position: :dodge, size: 6, nudge_y: 4)
78 | |> Plot.plot()
79 |
80 | ```
81 | 
82 | ```
83 | # Position label in the middle of stacked bars
84 | [%{x: "1", y: 1, grp: "a"},
85 | %{x: "1", y: 3, grp: "b"},
86 | %{x: "2", y: 2, grp: "a"},
87 | %{x: "2", y: 1, grp: "b"},]
88 | |> Plot.new(%{x: "x", y: "y", group: "grp"})
89 | |> Plot.geom_col(%{fill: "grp"})
90 | |> Plot.geom_text(%{label: "y"}, position: :stack, position_vjust: 0.5, size: 6)
91 | |> Plot.plot()
92 |
93 | ```
94 | 
95 |
--------------------------------------------------------------------------------
/guides/scale_color_viridis.md:
--------------------------------------------------------------------------------
1 | ```
2 | # The viridis scale is the default color scale
3 | Examples.diamonds()
4 | |> Explorer.DataFrame.sample(1000, seed: 100)
5 | |> Plot.new(%{x: "carat", y: "price"})
6 | |> Plot.geom_point(%{color: "clarity"})
7 | |> Plot.plot()
8 |
9 | ```
10 | 
11 | ```
12 | # Use the :option option to select a palette
13 | cities = Explorer.Series.from_list([
14 | "Houston",
15 | "Fort Worth",
16 | "San Antonio",
17 | "Dallas",
18 | "Austin"
19 | ])
20 |
21 | Examples.tx_housing()
22 | |> Explorer.DataFrame.filter_with(&Explorer.Series.in(&1["city"], cities))
23 | |> Plot.new(%{x: "sales", y: "median"})
24 | |> Plot.geom_point(%{color: "city"})
25 | |> Plot.scale_color_viridis(option: :plasma)
26 | |> Plot.plot()
27 |
28 | ```
29 | 
30 | ```
31 | cities = Explorer.Series.from_list([
32 | "Houston",
33 | "Fort Worth",
34 | "San Antonio",
35 | "Dallas",
36 | "Austin"
37 | ])
38 |
39 | Examples.tx_housing()
40 | |> Explorer.DataFrame.filter_with(&Explorer.Series.in(&1["city"], cities))
41 | |> Plot.new(%{x: "sales", y: "median"})
42 | |> Plot.geom_point(%{color: "city"})
43 | |> Plot.scale_color_viridis(option: :inferno)
44 | |> Plot.plot()
45 |
46 | ```
47 | 
48 |
--------------------------------------------------------------------------------
/guides/theme.md:
--------------------------------------------------------------------------------
1 | ```
2 | Examples.mtcars()
3 | |> Plot.new(%{x: :wt, y: :mpg})
4 | |> Plot.geom_point()
5 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
6 | |> Plot.plot()
7 |
8 | ```
9 | 
10 | ```
11 | # Examples below assume that element constructors are imported
12 | # e.g. `import GGity.Element.{Line, Rect, Text}
13 |
14 | # Plot formatting
15 | Examples.mtcars()
16 | |> Plot.new(%{x: :wt, y: :mpg})
17 | |> Plot.geom_point()
18 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
19 | |> Plot.theme(plot_title: element_text(size: 10))
20 | |> Plot.plot()
21 |
22 | ```
23 | 
24 | ```
25 | Examples.mtcars()
26 | |> Plot.new(%{x: :wt, y: :mpg})
27 | |> Plot.geom_point()
28 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
29 | |> Plot.theme(plot_background: element_rect(fill: "green"))
30 | |> Plot.plot()
31 |
32 | ```
33 | 
34 | ```
35 | # Panel formatting
36 | Examples.mtcars()
37 | |> Plot.new(%{x: :wt, y: :mpg})
38 | |> Plot.geom_point()
39 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
40 | |> Plot.theme(panel_background: element_rect(fill: "white", color: "grey"))
41 | |> Plot.plot()
42 |
43 | ```
44 | 
45 | ```
46 | Examples.mtcars()
47 | |> Plot.new(%{x: :wt, y: :mpg})
48 | |> Plot.geom_point()
49 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
50 | |> Plot.theme(panel_grid_major: element_line(color: "black"))
51 | |> Plot.plot()
52 |
53 | ```
54 | 
55 | ```
56 | # Axis formatting
57 | Examples.mtcars()
58 | |> Plot.new(%{x: :wt, y: :mpg})
59 | |> Plot.geom_point()
60 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
61 | |> Plot.theme(axis_line: element_line(size: 6, color: "grey"))
62 | |> Plot.plot()
63 |
64 | ```
65 | 
66 | ```
67 | Examples.mtcars()
68 | |> Plot.new(%{x: :wt, y: :mpg})
69 | |> Plot.geom_point()
70 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
71 | |> Plot.theme(axis_text: element_text(color: "blue"))
72 | |> Plot.plot()
73 |
74 | ```
75 | 
76 | ```
77 | Examples.mtcars()
78 | |> Plot.new(%{x: :wt, y: :mpg})
79 | |> Plot.geom_point()
80 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
81 | |> Plot.theme(axis_ticks: element_line(size: 4))
82 | |> Plot.plot()
83 |
84 | ```
85 | 
86 | ```
87 | # Turn the x-axis ticks inward
88 | Examples.mtcars()
89 | |> Plot.new(%{x: :wt, y: :mpg})
90 | |> Plot.geom_point()
91 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
92 | |> Plot.theme(axis_ticks_length_x: -2)
93 | |> Plot.plot()
94 |
95 | ```
96 | 
97 | ```
98 | # GGity does not support legend position, but legend key boxes
99 | # and text can be styled as you would expect
100 |
101 | # Default styling
102 | Examples.mtcars()
103 | |> Plot.new(%{x: :wt, y: :mpg})
104 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
105 | |> Plot.labs(
106 | x: "Weight (1000 lbs)",
107 | y: "Fuel economy (mpg)",
108 | color: "Cylinders",
109 | shape: "Transmission"
110 | )
111 | |> Plot.plot()
112 |
113 | ```
114 | 
115 | ```
116 | # Style legend keys
117 | Examples.mtcars()
118 | |> Plot.new(%{x: :wt, y: :mpg})
119 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
120 | |> Plot.labs(
121 | x: "Weight (1000 lbs)",
122 | y: "Fuel economy (mpg)",
123 | color: "Cylinders",
124 | shape: "Transmission"
125 | )
126 | |> Plot.theme(legend_key: element_rect(fill: "white", color: "black"))
127 | |> Plot.plot()
128 |
129 | ```
130 | 
131 | ```
132 | # Style legend text
133 | Examples.mtcars()
134 | |> Plot.new(%{x: :wt, y: :mpg})
135 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
136 | |> Plot.labs(
137 | x: "Weight (1000 lbs)",
138 | y: "Fuel economy (mpg)",
139 | color: "Cylinders",
140 | shape: "Transmission"
141 | )
142 | |> Plot.theme(legend_text: element_text(size: 4, color: "red"))
143 | |> Plot.plot()
144 |
145 | ```
146 | 
147 | ```
148 | # Style legend title
149 | Examples.mtcars()
150 | |> Plot.new(%{x: :wt, y: :mpg})
151 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
152 | |> Plot.labs(
153 | x: "Weight (1000 lbs)",
154 | y: "Fuel economy (mpg)",
155 | color: "Cylinders",
156 | shape: "Transmission"
157 | )
158 | |> Plot.theme(legend_title: element_text(face: "bold"))
159 | |> Plot.plot()
160 |
161 | ```
162 | 
163 |
--------------------------------------------------------------------------------
/lib/ggity.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity do
2 | @moduledoc false
3 | end
4 |
--------------------------------------------------------------------------------
/lib/ggity/annotate.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Annotate do
2 | @moduledoc false
3 |
4 | alias GGity.{Geom, Layer}
5 |
6 | @type geom :: Geom.Rect.t() | Geom.Segment.t() | Geom.Text.t()
7 |
8 | @supported_geoms [:rect, :segment, :text]
9 |
10 | @required_parameters [
11 | rect: [:xmin, :xmax, :ymin, :ymax],
12 | segment: [:x, :xend, :y, :yend],
13 | text: [:x, :y, :label]
14 | ]
15 |
16 | @geom_structs [
17 | rect: %Geom.Rect{},
18 | segment: %Geom.Segment{},
19 | text: %Geom.Text{}
20 | ]
21 |
22 | @doc false
23 | @spec annotate(atom(), keyword()) :: geom()
24 | def annotate(geom_type, params) when geom_type in @supported_geoms do
25 | mapping = required_params_mapping(geom_type, params)
26 | options = construct_options(geom_type, params)
27 | Layer.new(@geom_structs[geom_type], mapping, options)
28 | end
29 |
30 | defp required_params_mapping(type, params) do
31 | for {key, _} <- params, key in required_parameters(type), do: {key, to_string(key)}, into: %{}
32 | end
33 |
34 | defp construct_options(type, params) do
35 | options = for {key, param} <- params, key not in required_parameters(type), do: {key, param}
36 | Keyword.put(options, :data, format_data(type, params))
37 | end
38 |
39 | defp format_data(type, params) do
40 | row =
41 | for {key, _} <- params,
42 | key in required_parameters(type),
43 | do: {to_string(key), params[key]},
44 | into: %{}
45 |
46 | Explorer.DataFrame.new([row])
47 | end
48 |
49 | defp required_parameters(type), do: @required_parameters[type]
50 | end
51 |
--------------------------------------------------------------------------------
/lib/ggity/color.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Color do
2 | @moduledoc false
3 |
4 | @valid_css_color_names [
5 | "aliceblue",
6 | "antiquewhite",
7 | "aqua",
8 | "aquamarine",
9 | "azure",
10 | "beige",
11 | "bisque",
12 | "black",
13 | "blanchedalmond",
14 | "blue",
15 | "blueviolet",
16 | "brown",
17 | "burlywood",
18 | "cadetblue",
19 | "chartreuse",
20 | "chocolate",
21 | "coral",
22 | "cornflowerblue",
23 | "cornsilk",
24 | "crimson",
25 | "cyan",
26 | "darkblue",
27 | "darkcyan",
28 | "darkgoldenrod",
29 | "darkgray",
30 | "darkgrey",
31 | "darkgreen",
32 | "darkkhaki",
33 | "darkmagenta",
34 | "darkolivegreen",
35 | "darkorange",
36 | "darkorchid",
37 | "darkred",
38 | "darksalmon",
39 | "darkseagreen",
40 | "darkslateblue",
41 | "darkslategray",
42 | "darkslategrey",
43 | "darkturquoise",
44 | "darkviolet",
45 | "deeppink",
46 | "deepskyblue",
47 | "dimgray",
48 | "dimgrey",
49 | "dodgerblue",
50 | "firebrick",
51 | "floralwhite",
52 | "forestgreen",
53 | "fuchsia",
54 | "gainsboro",
55 | "ghostwhite",
56 | "gold",
57 | "goldenrod",
58 | "gray",
59 | "grey",
60 | "green",
61 | "greenyellow",
62 | "honeydew",
63 | "hotpink",
64 | "indianred",
65 | "indigo",
66 | "ivory",
67 | "khaki",
68 | "lavender",
69 | "lavenderblush",
70 | "lawngreen",
71 | "lemonchiffon",
72 | "lightblue",
73 | "lightcoral",
74 | "lightcyan",
75 | "lightgoldenrodyellow",
76 | "lightgray",
77 | "lightgrey",
78 | "lightgreen",
79 | "lightpink",
80 | "lightsalmon",
81 | "lightseagreen",
82 | "lightskyblue",
83 | "lightslategray",
84 | "lightslategrey",
85 | "lightsteelblue",
86 | "lightyellow",
87 | "lime",
88 | "limegreen",
89 | "linen",
90 | "magenta",
91 | "maroon",
92 | "mediumaquamarine",
93 | "mediumblue",
94 | "mediumorchid",
95 | "mediumpurple",
96 | "mediumseagreen",
97 | "mediumslateblue",
98 | "mediumspringgreen",
99 | "mediumturquoise",
100 | "mediumvioletred",
101 | "midnightblue",
102 | "mintcream",
103 | "mistyrose",
104 | "moccasin",
105 | "navajowhite",
106 | "navy",
107 | "oldlace",
108 | "olive",
109 | "olivedrab",
110 | "orange",
111 | "orangered",
112 | "orchid",
113 | "palegoldenrod",
114 | "palegreen",
115 | "paleturquoise",
116 | "palevioletred",
117 | "papayawhip",
118 | "peachpuff",
119 | "peru",
120 | "pink",
121 | "plum",
122 | "powderblue",
123 | "purple",
124 | "rebeccapurple",
125 | "red",
126 | "rosybrown",
127 | "royalblue",
128 | "saddlebrown",
129 | "salmon",
130 | "sandybrown",
131 | "seagreen",
132 | "seashell",
133 | "sienna",
134 | "silver",
135 | "skyblue",
136 | "slateblue",
137 | "slategray",
138 | "slategrey",
139 | "snow",
140 | "springgreen",
141 | "steelblue",
142 | "tan",
143 | "teal",
144 | "thistle",
145 | "tomato",
146 | "turquoise",
147 | "violet",
148 | "wheat",
149 | "white",
150 | "whitesmoke",
151 | "yellow",
152 | "yellowgreen"
153 | ]
154 |
155 | # TODO: write some tests for this
156 | @doc false
157 | @spec valid_color?(any()) :: boolean()
158 | def valid_color?(<>) when code not in @valid_css_color_names do
159 | String.match?(code, ~r/^[[:xdigit:]]{3}$/)
160 | end
161 |
162 | def valid_color?(<>) when code not in @valid_css_color_names do
163 | String.match?(code, ~r/^[[:xdigit:]]{6}$/)
164 | end
165 |
166 | def valid_color?("#" <> code) do
167 | valid_color?(code)
168 | end
169 |
170 | def valid_color?(color) when is_binary(color) do
171 | String.downcase(color) in @valid_css_color_names
172 | end
173 |
174 | def valid_color?(_color), do: false
175 | end
176 |
--------------------------------------------------------------------------------
/lib/ggity/draw.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Draw do
2 | @moduledoc false
3 |
4 | alias GGity.HTML
5 |
6 | @type options() :: keyword()
7 |
8 | @spec svg(iolist(), options()) :: iolist()
9 | def svg(elements, options \\ []) do
10 | [
11 | ~s|"
17 | ]
18 | end
19 |
20 | @spec g(iolist(), options()) :: iolist()
21 | def g(elements, options) do
22 | attributes = options_to_attributes(options)
23 |
24 | [
25 | "",
28 | "\n",
29 | elements,
30 | "",
31 | "\n"
32 | ]
33 | end
34 |
35 | @spec rect(options()) :: iolist()
36 | def rect(options) do
37 | [
38 | "",
41 | "",
42 | "\n"
43 | ]
44 | end
45 |
46 | @spec line(options()) :: iolist()
47 | def line(coord_list) do
48 | [
49 | "",
52 | "",
53 | "\n"
54 | ]
55 | end
56 |
57 | @spec text(binary(), options()) :: iolist()
58 | def text(text_element, options) do
59 | attributes = options_to_attributes(options)
60 |
61 | [
62 | "",
65 | HTML.escape_to_iodata(text_element),
66 | "",
67 | "\n"
68 | ]
69 | end
70 |
71 | @spec circle({number(), number()}, number(), keyword()) :: iolist()
72 | def circle({x, y}, radius, options) do
73 | [
74 | "",
86 | "",
87 | "\n"
88 | ]
89 | end
90 |
91 | @spec polygon(binary(), keyword()) :: iolist()
92 | def polygon(points, options) do
93 | [
94 | "",
98 | "\n"
99 | ]
100 | end
101 |
102 | @spec polyline(list({number(), number()}), binary(), number(), number(), binary()) :: iolist()
103 | def polyline(coords, color, size, alpha, linetype) do
104 | coord_list = Enum.map(coords, fn {x, y} -> [to_string(x), ",", to_string(y), " "] end)
105 |
106 | [
107 | "",
124 | "",
125 | "\n"
126 | ]
127 | end
128 |
129 | defp options_to_attributes(options) do
130 | Enum.map_join(options, " ", &option_to_attribute/1)
131 | end
132 |
133 | defp option_to_attribute({name, value}) do
134 | name =
135 | name
136 | |> Atom.to_string()
137 | |> String.replace("_", "-")
138 |
139 | [
140 | name,
141 | "=\"",
142 | HTML.escape_to_iodata(to_string(value)),
143 | "\""
144 | ]
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/lib/ggity/element.ex:
--------------------------------------------------------------------------------
1 | defprotocol GGity.Element do
2 | @moduledoc false
3 |
4 | @spec to_css(GGity.Element.t(), binary()) :: iolist()
5 | def to_css(element, class)
6 | end
7 |
8 | defimpl GGity.Element, for: Atom do
9 | def to_css(nil, _class), do: []
10 | end
11 |
12 | defimpl GGity.Element, for: Integer do
13 | def to_css(_element, _class), do: []
14 | end
15 |
16 | defimpl GGity.Element, for: Any do
17 | def to_css(%element_type{} = element, class) do
18 | [".", class, " {", apply(element_type, :attributes_for, [element]), "}"]
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/ggity/element/line.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Element.Line do
2 | @moduledoc """
3 | Defines the data and functions used to style non-geom line elements.
4 |
5 | CSS presentation attributes:
6 | * `:color` sets value of CSS `stroke`
7 | * `:size` sets value of CSS `stroke-width`
8 | """
9 |
10 | import GGity.Color, only: [valid_color?: 1]
11 | alias GGity.Element
12 |
13 | @derive [Element]
14 | defstruct [
15 | :color,
16 | :size
17 | # :linetype,
18 | # :lineend,
19 | # :arrow
20 | ]
21 |
22 | @type t() :: %__MODULE__{}
23 |
24 | @doc """
25 | Constructor for a Line element.
26 |
27 | Setting the value of an attributed to `nil` will remove that property
28 | from the generated stylesheet altogether.
29 |
30 | Calling `element_line(attributes)` is equivalent to `struct(GGity.Element.Line, attributes)`.
31 | """
32 | @spec element_line(keyword()) :: Element.Line.t()
33 | def element_line(attributes) do
34 | struct(Element.Line, attributes)
35 | end
36 |
37 | @doc false
38 | @spec attributes_for(Element.Line.t()) :: iolist()
39 | def attributes_for(element) do
40 | element
41 | |> Map.from_struct()
42 | |> Enum.map(&attribute_for/1)
43 | end
44 |
45 | defp attribute_for({_attribute, nil}), do: []
46 |
47 | defp attribute_for({:color, value}) do
48 | if valid_color?(value), do: ["stroke: ", value, ";"], else: []
49 | end
50 |
51 | defp attribute_for({:size, value}) when is_number(value) do
52 | ["stroke-width: ", to_string(value), ";"]
53 | end
54 |
55 | defp attribute_for({:size, _value}), do: []
56 |
57 | defp attribute_for(_element), do: []
58 | end
59 |
--------------------------------------------------------------------------------
/lib/ggity/element/rect.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Element.Rect do
2 | @moduledoc """
3 | Defines the data and functions used to style non-geom rect elements.
4 |
5 | CSS presentation attributes:
6 | * `:fill` - string: sets value of CSS `fill`
7 |
8 | Values must be valid CSS color names or hex values.
9 |
10 | * `:color` - string: sets value of CSS `stroke`
11 |
12 | Values must be valid CSS color names or hex values.
13 |
14 | * `:size` - number: sets value of CSS `stroke-width`
15 |
16 |
17 | Other attributes:
18 | * `:height` - number: sets value of SVG `height` (height of the key glyph box)
19 | """
20 |
21 | import GGity.Color, only: [valid_color?: 1]
22 | alias GGity.Element
23 |
24 | @derive [Element]
25 | defstruct [
26 | :fill,
27 | :color,
28 | :size,
29 | # :linetype,
30 | :height
31 | ]
32 |
33 | @type t() :: %__MODULE__{}
34 |
35 | @doc """
36 | Constructor for a Rect element.
37 |
38 | Setting the value of an attributed to `nil` will remove that property
39 | from the generated stylesheet altogether.
40 |
41 | Calling `element_rect(attributes)` is equivalent to `struct(GGity.Element.Line, attributes)`.
42 | """
43 | @spec element_rect(keyword()) :: Element.Line.t()
44 | def element_rect(attributes) do
45 | struct(Element.Rect, attributes)
46 | end
47 |
48 | @doc false
49 | @spec attributes_for(Element.Rect.t()) :: iolist()
50 | def attributes_for(element) do
51 | element
52 | |> Map.from_struct()
53 | |> Enum.map(&attribute_for/1)
54 | end
55 |
56 | defp attribute_for({_attribute, nil}), do: []
57 |
58 | defp attribute_for({:fill, value}) do
59 | if valid_color?(value), do: ["fill: ", value, ";"], else: []
60 | end
61 |
62 | defp attribute_for({:color, value}) do
63 | if valid_color?(value), do: ["stroke: ", value, ";"], else: []
64 | end
65 |
66 | defp attribute_for({:size, value}) when is_number(value) do
67 | ["stroke-width: ", to_string(value), ";"]
68 | end
69 |
70 | defp attribute_for(_element), do: []
71 | end
72 |
--------------------------------------------------------------------------------
/lib/ggity/element/text.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Element.Text do
2 | @moduledoc """
3 | Defines the data and functions used to style non-geom text elements.
4 |
5 | CSS presentation attributes:
6 | * `:family` - string: sets value of CSS `font-family`
7 |
8 | * `:face` - string or integer: sets value of CSS `font-weight`
9 |
10 | Valid values:
11 | * `"normal"`
12 | * `"bold"`
13 | * `"bolder"`
14 | * `"lighter"`
15 | * `"initial"`
16 | * `"inherit"`
17 | * A multiple of 100 between 100 and 900
18 |
19 | * `:color` - string: sets value of CSS `fill`
20 |
21 | Values must be valid CSS color names or hex values.
22 |
23 | * `:size` - number: sets value of CSS `font-size` in pixels
24 |
25 | Other attributes:
26 | * `:angle` - number (between 0 and 90): sets the value passed to
27 | `transform: rotate()` for the purpose of rotating x axis tick
28 | labels (has no effect when set for other theme elements)
29 | """
30 |
31 | import GGity.Color, only: [valid_color?: 1]
32 | alias GGity.{Element, HTML}
33 |
34 | @valid_font_weights List.flatten([
35 | "normal",
36 | "bold",
37 | "bolder",
38 | "lighter",
39 | "initial",
40 | "inherit",
41 | Enum.map(1..9, fn number -> [number * 100, to_string(number * 100)] end)
42 | ])
43 |
44 | @derive [Element]
45 | defstruct [
46 | :family,
47 | :face,
48 | :color,
49 | :size,
50 | :angle
51 | # :hjust,
52 | # :vjust,
53 | # :lineheight,
54 | # :margin
55 | ]
56 |
57 | @type t() :: %__MODULE__{}
58 |
59 | @doc """
60 | Constructor for a Text element.
61 |
62 | Setting the value of an attributed to `nil` will remove that property
63 | from the generated stylesheet altogether.
64 |
65 | Calling `element_text(attributes)` is equivalent to `struct(GGity.Element.Text, attributes)`.
66 | """
67 | @spec element_text(keyword()) :: Element.Text.t()
68 | def element_text(attributes) do
69 | struct(Element.Text, attributes)
70 | end
71 |
72 | @doc false
73 | @spec attributes_for(Element.Text.t()) :: iolist()
74 | def attributes_for(element) do
75 | element
76 | |> Map.from_struct()
77 | |> Enum.map(&attribute_for/1)
78 | end
79 |
80 | defp attribute_for({_attribute, nil}), do: []
81 |
82 | defp attribute_for({:family, value}) do
83 | ["font-family: ", HTML.escape_to_iodata(value), ";"]
84 | end
85 |
86 | defp attribute_for({:face, value}) when value in @valid_font_weights do
87 | ["font-weight: ", value, ";"]
88 | end
89 |
90 | defp attribute_for({:color, value}) when is_binary(value) do
91 | if valid_color?(value), do: ["fill: ", value, ";"]
92 | end
93 |
94 | defp attribute_for({:size, value}) when is_number(value) do
95 | ["font-size: ", to_string(value), "px;"]
96 | end
97 |
98 | defp attribute_for(_element), do: []
99 | end
100 |
--------------------------------------------------------------------------------
/lib/ggity/geom/bar.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Bar do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Plot}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type record() :: map()
8 | @type mapping() :: map()
9 |
10 | defstruct data: nil,
11 | mapping: nil,
12 | stat: :count,
13 | position: :stack,
14 | key_glyph: :rect,
15 | fill: "black",
16 | alpha: 1,
17 | bar_group_width: nil,
18 | custom_attributes: nil
19 |
20 | @spec new(mapping(), keyword()) :: Geom.Bar.t()
21 | def new(mapping, options \\ []) do
22 | struct(Geom.Bar, [{:mapping, mapping} | options])
23 | end
24 |
25 | @spec draw(Geom.Bar.t(), list(map()), Plot.t()) :: iolist()
26 | def draw(%Geom.Bar{} = geom_bar, data, plot) do
27 | number_of_levels = length(plot.scales.x.levels)
28 | group_width = (plot.width - number_of_levels * (plot.scales.x.padding - 1)) / number_of_levels
29 | geom_bar = struct(geom_bar, bar_group_width: group_width)
30 | bars(geom_bar, data, plot)
31 | end
32 |
33 | defp bars(%Geom.Bar{} = geom_bar, data, plot) do
34 | data
35 | |> Enum.reject(fn row -> row[geom_bar.mapping[:y]] == 0 end)
36 | |> Enum.group_by(fn row -> row[geom_bar.mapping[:x]] end)
37 | |> Enum.with_index(fn {_x_value, group}, group_index ->
38 | bar_group(geom_bar, group, group_index, plot)
39 | end)
40 | end
41 |
42 | defp bar_group(geom_bar, group_values, group_index, %Plot{scales: scales} = plot) do
43 | transforms = transforms(geom_bar, scales)
44 | count_rows = length(group_values)
45 |
46 | sort_order =
47 | case geom_bar.position do
48 | :stack -> :desc
49 | :dodge -> :asc
50 | _unknown_adjustment -> :asc
51 | end
52 |
53 | group_values
54 | |> Enum.sort_by(
55 | fn row -> {row[geom_bar.mapping[:fill]], row[geom_bar.mapping[:alpha]]} end,
56 | sort_order
57 | )
58 | |> Enum.reduce({0, 0, []}, fn row, {total_width, total_height, rects} ->
59 | custom_attributes = GGity.Layer.custom_attributes(geom_bar, plot, row)
60 |
61 | {
62 | total_width + geom_bar.bar_group_width / count_rows,
63 | total_height +
64 | transforms.y.(row[geom_bar.mapping[:y]]) / plot.aspect_ratio,
65 | [
66 | Draw.rect(
67 | [
68 | x: position_adjust_x(geom_bar, row, group_index, total_width, plot),
69 | y:
70 | plot.area_padding + plot.width / plot.aspect_ratio -
71 | position_adjust_y(geom_bar, row, total_height, plot),
72 | width: position_adjust_bar_width(geom_bar, count_rows),
73 | height: transforms.y.(row[geom_bar.mapping[:y]]) / plot.aspect_ratio,
74 | fill: transforms.fill.(row[geom_bar.mapping[:fill]]),
75 | fill_opacity: transforms.alpha.(row[geom_bar.mapping[:alpha]])
76 | ] ++
77 | custom_attributes
78 | )
79 | | rects
80 | ]
81 | }
82 | end)
83 | |> elem(2)
84 | end
85 |
86 | defp transforms(geom_bar, scales) do
87 | scale_transforms =
88 | geom_bar.mapping
89 | |> Map.keys()
90 | |> Enum.reduce(%{}, fn aesthetic, mapped ->
91 | Map.put(mapped, aesthetic, Map.get(scales[aesthetic], :transform))
92 | end)
93 |
94 | geom_bar
95 | |> Map.take([:alpha, :fill])
96 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
97 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
98 | end)
99 | |> Map.merge(scale_transforms)
100 | end
101 |
102 | defp position_adjust_x(
103 | %Geom.Bar{position: :stack} = geom_bar,
104 | _row,
105 | group_index,
106 | _total_width,
107 | plot
108 | ) do
109 | plot.area_padding + group_index * (geom_bar.bar_group_width + plot.scales.x.padding)
110 | end
111 |
112 | defp position_adjust_x(
113 | %Geom.Bar{position: :dodge} = geom_bar,
114 | _row,
115 | group_index,
116 | total_width,
117 | plot
118 | ) do
119 | plot.area_padding + group_index * (geom_bar.bar_group_width + plot.scales.x.padding) +
120 | total_width
121 | end
122 |
123 | defp position_adjust_y(%Geom.Bar{position: :stack} = geom_bar, row, total_height, plot) do
124 | total_height + plot.scales.y.transform.(row[geom_bar.mapping[:y]]) / plot.aspect_ratio
125 | end
126 |
127 | defp position_adjust_y(%Geom.Bar{position: :dodge} = geom_bar, row, _total_height, plot) do
128 | plot.scales.y.transform.(row[geom_bar.mapping[:y]]) / plot.aspect_ratio
129 | end
130 |
131 | defp position_adjust_bar_width(%Geom.Bar{position: :stack} = geom_bar, _count_rows) do
132 | geom_bar.bar_group_width
133 | end
134 |
135 | defp position_adjust_bar_width(%Geom.Bar{position: :dodge} = geom_bar, count_rows) do
136 | geom_bar.bar_group_width / count_rows
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/lib/ggity/geom/blank.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Blank do
2 | @moduledoc false
3 |
4 | defstruct data: nil,
5 | mapping: nil,
6 | position: :identity,
7 | stat: :identity,
8 | labels: %{}
9 |
10 | @spec draw() :: iolist()
11 | def draw, do: []
12 | end
13 |
--------------------------------------------------------------------------------
/lib/ggity/geom/boxplot.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Boxplot do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Plot, Shapes}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type record() :: map()
8 | @type mapping() :: map()
9 |
10 | defstruct data: nil,
11 | mapping: nil,
12 | stat: :boxplot,
13 | position: :dodge,
14 | key_glyph: :boxplot,
15 | outlier_color: nil,
16 | outlier_fill: nil,
17 | outlier_shape: :circle,
18 | outlier_size: 5,
19 | color: "black",
20 | fill: "white",
21 | alpha: 1,
22 | box_group_width: nil,
23 | custom_attributes: nil
24 |
25 | @spec new(mapping(), keyword()) :: Geom.Boxplot.t()
26 | def new(mapping, options \\ []) do
27 | struct(Geom.Boxplot, [{:mapping, mapping} | options])
28 | end
29 |
30 | @spec draw(Geom.Boxplot.t(), list(map()), Plot.t()) :: iolist
31 | def draw(%Geom.Boxplot{} = geom_boxplot, data, plot) do
32 | number_of_levels = length(plot.scales.x.levels)
33 | group_width = (plot.width - number_of_levels * (plot.scales.x.padding - 1)) / number_of_levels
34 | geom_boxplot = struct(geom_boxplot, box_group_width: group_width)
35 | boxplots(geom_boxplot, data, plot)
36 | end
37 |
38 | defp boxplots(%Geom.Boxplot{} = geom_boxplot, data, plot) do
39 | data
40 | |> Enum.group_by(fn row -> row[geom_boxplot.mapping[:x]] end)
41 | |> Enum.with_index(fn {_x_value, group_values}, group_index ->
42 | boxplot_group(geom_boxplot, group_values, group_index, plot)
43 | end)
44 | end
45 |
46 | defp boxplot_group(geom_boxplot, group_values, group_index, %Plot{scales: scales} = plot) do
47 | mapping = geom_boxplot.mapping
48 | scale_transforms = fetch_scale_transforms(mapping, scales)
49 | fixed_aesthetics = fetch_fixed_aesthetics(geom_boxplot)
50 | transforms = Map.merge(fixed_aesthetics, scale_transforms)
51 |
52 | count_rows = length(group_values)
53 |
54 | group_values
55 | |> Enum.sort_by(
56 | fn row ->
57 | {row[mapping[:fill]], row[mapping[:color]], row[mapping[:alpha]]}
58 | end,
59 | :asc
60 | )
61 | |> Enum.reduce({0, []}, fn row, {total_width, rects} ->
62 | box_left = position_adjust_x(geom_boxplot, row, group_index, total_width, plot)
63 | box_width = position_adjust_bar_width(geom_boxplot, count_rows)
64 | box_right = box_left + box_width
65 | box_middle = box_left + box_width / 2
66 |
67 | {
68 | total_width + geom_boxplot.box_group_width / count_rows,
69 | [
70 | Draw.rect(
71 | [
72 | x: box_left,
73 | y:
74 | plot.area_padding + plot.width / plot.aspect_ratio -
75 | position_adjust_y(row, plot),
76 | width: box_width,
77 | height:
78 | (transforms.y.(row["upper"]) - transforms.y.(row["lower"])) / plot.aspect_ratio,
79 | fill: transforms.fill.(row[mapping[:fill]]),
80 | fill_opacity: transforms.alpha.(row[mapping[:alpha]]),
81 | stroke: transforms.color.(row[mapping[:color]]),
82 | stroke_width: 0.5
83 | ] ++ GGity.Layer.custom_attributes(geom_boxplot, plot, row)
84 | ),
85 | draw_median(box_left, box_right, row, transforms, mapping, plot),
86 | draw_top_whisker(box_middle, row, transforms, mapping, plot),
87 | draw_bottom_whisker(box_middle, row, transforms, mapping, plot),
88 | for outlier <- :erlang.binary_to_term(row["outliers"]) do
89 | draw_outlier(outlier, box_middle, row, geom_boxplot, transforms, plot)
90 | end
91 | | rects
92 | ]
93 | }
94 | end)
95 | |> elem(1)
96 | end
97 |
98 | defp fetch_scale_transforms(mapping, scales) do
99 | for aes <- Map.keys(mapping), reduce: %{} do
100 | scale_transforms -> Map.put(scale_transforms, aes, scales[aes].transform)
101 | end
102 | end
103 |
104 | defp fetch_fixed_aesthetics(geom_boxplot) do
105 | geom_boxplot
106 | |> Map.take([:alpha, :color, :fill])
107 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
108 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
109 | end)
110 | end
111 |
112 | defp draw_median(box_left, box_right, row, transforms, mapping, plot) do
113 | Draw.line(
114 | x1: box_left,
115 | x2: box_right,
116 | y1: transform_and_adjust_y(row, "middle", plot),
117 | y2: transform_and_adjust_y(row, "middle", plot),
118 | stroke: transforms.color.(row[mapping[:color]])
119 | )
120 | end
121 |
122 | defp draw_top_whisker(box_middle, row, transforms, mapping, plot) do
123 | Draw.line(
124 | x1: box_middle,
125 | x2: box_middle,
126 | y1: transform_and_adjust_y(row, "upper", plot),
127 | y2: transform_and_adjust_y(row, "ymax", plot),
128 | stroke: transforms.color.(row[mapping[:color]]),
129 | stroke_width: 0.5
130 | )
131 | end
132 |
133 | defp draw_bottom_whisker(box_middle, row, transforms, mapping, plot) do
134 | Draw.line(
135 | x1: box_middle,
136 | x2: box_middle,
137 | y1: transform_and_adjust_y(row, "lower", plot),
138 | y2: transform_and_adjust_y(row, "ymin", plot),
139 | stroke: transforms.color.(row[mapping[:color]]),
140 | stroke_width: 0.5
141 | )
142 | end
143 |
144 | defp transform_and_adjust_y(row, aes, plot) do
145 | plot.area_padding + plot.width / plot.aspect_ratio -
146 | plot.scales.y.transform.(row[aes]) / plot.aspect_ratio
147 | end
148 |
149 | defp draw_outlier(value, box_middle, row, geom_boxplot, transforms, plot) do
150 | y_coord = plot.area_padding + (200 - plot.scales.y.transform.(value)) / plot.aspect_ratio
151 | fill = geom_boxplot.outlier_fill || transforms.color.(row[geom_boxplot.mapping[:color]])
152 | color = geom_boxplot.outlier_color || transforms.color.(row[geom_boxplot.mapping[:color]])
153 |
154 | # This will break when we have fillable shapes
155 | case geom_boxplot.outlier_shape do
156 | :na ->
157 | []
158 |
159 | :circle ->
160 | Shapes.draw(:circle, {box_middle, y_coord}, :math.pow(geom_boxplot.outlier_size, 2),
161 | fill: fill,
162 | color: color
163 | )
164 |
165 | shape ->
166 | Shapes.draw(shape, {box_middle, y_coord}, :math.pow(geom_boxplot.outlier_size, 2),
167 | fill: fill,
168 | color: color
169 | )
170 | end
171 | end
172 |
173 | defp position_adjust_x(
174 | %Geom.Boxplot{position: :dodge} = geom_boxplot,
175 | _row,
176 | group_index,
177 | total_width,
178 | plot
179 | ) do
180 | plot.area_padding + group_index * (geom_boxplot.box_group_width + plot.scales.x.padding) +
181 | total_width
182 | end
183 |
184 | defp position_adjust_y(row, plot) do
185 | plot.scales.y.transform.(row["upper"]) / plot.aspect_ratio
186 | end
187 |
188 | defp position_adjust_bar_width(%Geom.Boxplot{position: :dodge} = geom_box, count_rows) do
189 | geom_box.box_group_width / count_rows
190 | end
191 | end
192 |
--------------------------------------------------------------------------------
/lib/ggity/geom/line.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Line do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Plot}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type plot() :: %GGity.Plot{}
8 | @type record() :: map()
9 | @type mapping() :: map()
10 |
11 | @linetype_specs %{
12 | solid: "",
13 | dashed: "4",
14 | dotted: "1",
15 | longdash: "6 2",
16 | dotdash: "1 2 3 2",
17 | twodash: "2 2 6 2"
18 | }
19 |
20 | defstruct data: nil,
21 | mapping: nil,
22 | stat: :identity,
23 | position: :identity,
24 | key_glyph: :path,
25 | alpha: 1,
26 | color: "black",
27 | linetype: "",
28 | size: 1
29 |
30 | @spec new(mapping(), keyword()) :: Geom.Line.t()
31 | def new(mapping, options \\ []) do
32 | linetype_name = Keyword.get(options, :linetype, :solid)
33 |
34 | options =
35 | options
36 | |> Keyword.drop([:linetype])
37 | |> Keyword.merge(mapping: mapping, linetype: @linetype_specs[linetype_name])
38 |
39 | struct(Geom.Line, options)
40 | end
41 |
42 | @spec draw(Geom.Line.t(), list(map()), plot()) :: iolist()
43 | def draw(%Geom.Line{} = geom_line, _data, plot), do: lines(geom_line, plot)
44 |
45 | defp lines(%Geom.Line{} = geom_line, %Plot{} = plot) do
46 | scale_transforms = scale_transforms_for(geom_line, plot.scales)
47 |
48 | transforms =
49 | scale_transforms
50 | |> Map.put(:x, fn row ->
51 | transform_and_pad_x(row, scale_transforms.x, geom_line.mapping[:x], plot.area_padding)
52 | end)
53 | |> Map.put(:y, fn row ->
54 | transform_and_pad_y(
55 | row,
56 | scale_transforms.y,
57 | geom_line.mapping[:y],
58 | plot.area_padding,
59 | plot.aspect_ratio,
60 | plot.width
61 | )
62 | end)
63 |
64 | (geom_line.data || plot.data)
65 | |> Enum.group_by(fn row ->
66 | %{
67 | alpha: row[geom_line.mapping[:alpha]],
68 | color: row[geom_line.mapping[:color]],
69 | linetype: row[geom_line.mapping[:linetype]],
70 | size: row[geom_line.mapping[:size]]
71 | }
72 | end)
73 | |> Enum.map(fn {values, group} -> line(values, group, transforms) end)
74 | end
75 |
76 | defp scale_transforms_for(geom_line, scales) do
77 | scale_transforms = fetch_scale_transforms(geom_line.mapping, scales)
78 | fixed_aesthetics = fetch_fixed_aesthetics(geom_line)
79 | Map.merge(fixed_aesthetics, scale_transforms)
80 | end
81 |
82 | defp fetch_scale_transforms(mapping, scales) do
83 | for aes <- Map.keys(mapping), aes in [:x, :y, :color, :linetype, :size], reduce: %{} do
84 | scale_transforms -> Map.put(scale_transforms, aes, scales[aes].transform)
85 | end
86 | end
87 |
88 | defp fetch_fixed_aesthetics(geom_line) do
89 | geom_line
90 | |> Map.take([:alpha, :color, :linetype, :size])
91 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
92 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
93 | end)
94 | end
95 |
96 | defp transform_and_pad_x(row, x_transform, x_mapping, area_padding) do
97 | x_mapping = to_string(x_mapping)
98 | x_transform.(row[x_mapping]) + area_padding
99 | end
100 |
101 | defp transform_and_pad_y(row, y_transform, y_mapping, area_padding, aspect_ratio, width) do
102 | y_mapping = to_string(y_mapping)
103 | (width - y_transform.(row[y_mapping])) / aspect_ratio + area_padding
104 | end
105 |
106 | defp line(values, data, transforms) do
107 | coords =
108 | data
109 | |> Enum.map(fn row -> {transforms.x.(row), transforms.y.(row)} end)
110 | |> Enum.sort_by(fn {x, _y} -> x end)
111 |
112 | [alpha, color, linetype, size] = [
113 | transforms.alpha.(values.alpha),
114 | transforms.color.(values.color),
115 | transforms.linetype.(values.linetype),
116 | transforms.size.(values.size)
117 | ]
118 |
119 | Draw.polyline(coords, color, size, alpha, linetype)
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/lib/ggity/geom/point.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Point do
2 | @moduledoc false
3 |
4 | alias GGity.{Geom, Plot}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type plot() :: %Plot{}
8 | @type record() :: map()
9 | @type mapping() :: map()
10 |
11 | defstruct data: nil,
12 | mapping: nil,
13 | stat: :identity,
14 | position: :identity,
15 | key_glyph: :point,
16 | alpha: 1,
17 | color: "black",
18 | shape: :circle,
19 | size: 6,
20 | custom_attributes: nil
21 |
22 | @spec new(mapping(), keyword()) :: Geom.Point.t()
23 | def new(mapping, options) do
24 | struct(Geom.Point, [{:mapping, mapping} | options])
25 | end
26 |
27 | @spec draw(Geom.Point.t(), list(map()), plot()) :: iolist()
28 | def draw(%Geom.Point{} = geom_point, data, plot), do: points(geom_point, data, plot)
29 |
30 | defp points(%Geom.Point{} = geom_point, data, %Plot{scales: scales} = plot) do
31 | scale_transforms = fetch_scale_transforms(geom_point.mapping, scales)
32 | fixed_aesthetics = fetch_fixed_aesthetics(geom_point)
33 | all_transforms = Map.merge(fixed_aesthetics, scale_transforms)
34 |
35 | Enum.map(data, fn row -> point(row, all_transforms, geom_point, plot) end)
36 | end
37 |
38 | defp point(row, transforms, geom_point, plot) do
39 | mapping =
40 | Map.new(
41 | geom_point.mapping,
42 | fn {k, v} ->
43 | if is_atom(v) do
44 | {k, to_string(v)}
45 | else
46 | {k, v}
47 | end
48 | end
49 | )
50 |
51 | [x, y, alpha, color, shape, size] = [
52 | transforms.x.(row[mapping.x]),
53 | transforms.y.(row[mapping.y]),
54 | transforms.alpha.(row[mapping[:alpha]]),
55 | transforms.color.(row[mapping[:color]]),
56 | transforms.shape.(row[mapping[:shape]]),
57 | transforms.size.(row[mapping[:size]])
58 | ]
59 |
60 | adjusted_x = x + plot.area_padding
61 | adjusted_y = (plot.width - y) / plot.aspect_ratio + plot.area_padding
62 |
63 | custom_attributes = GGity.Layer.custom_attributes(geom_point, plot, row)
64 |
65 | options = [{:color, color}, {:fill_opacity, alpha} | custom_attributes]
66 |
67 | GGity.Shapes.draw(shape, {adjusted_x, adjusted_y}, size, options)
68 | end
69 |
70 | defp fetch_scale_transforms(mapping, scales) do
71 | mapping
72 | |> Map.keys()
73 | |> Map.new(fn aesthetic -> {aesthetic, scales[aesthetic].transform} end)
74 | end
75 |
76 | defp fetch_fixed_aesthetics(geom_point) do
77 | geom_point
78 | |> Map.take([:alpha, :color, :shape, :size])
79 | |> Enum.reduce(%{}, fn
80 | {:size, fixed_value}, fixed ->
81 | Map.put(fixed, :size, fn _value -> :math.pow(fixed_value, 2) end)
82 |
83 | {aesthetic, fixed_value}, fixed ->
84 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
85 | end)
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/lib/ggity/geom/rect.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Rect do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Plot}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type plot() :: %Plot{}
8 | @type record() :: map()
9 | @type mapping() :: map()
10 |
11 | defstruct data: nil,
12 | mapping: nil,
13 | stat: :identity,
14 | position: :identity,
15 | key_glyph: :rect,
16 | alpha: 1,
17 | fill: "black",
18 | color: "black",
19 | size: 0,
20 | custom_attributes: nil
21 |
22 | @spec new(mapping(), keyword()) :: Geom.Rect.t()
23 | def new(mapping, options) do
24 | struct(Geom.Rect, [{:mapping, mapping} | options])
25 | end
26 |
27 | @spec draw(Geom.Rect.t(), list(map()), plot()) :: iolist()
28 | def draw(%Geom.Rect{} = geom_rect, data, plot), do: rects(geom_rect, data, plot)
29 |
30 | defp rects(%Geom.Rect{} = geom_rect, data, %Plot{scales: scales} = plot) do
31 | scale_transforms = fetch_scale_transforms(geom_rect.mapping, scales)
32 | fixed_aesthetics = fetch_fixed_aesthetics(geom_rect)
33 | transforms = Map.merge(fixed_aesthetics, scale_transforms)
34 |
35 | Enum.map(data, fn row -> rect(row, transforms, geom_rect, plot) end)
36 | end
37 |
38 | defp fetch_scale_transforms(mapping, scales) do
39 | for aes <- Map.keys(mapping), reduce: %{} do
40 | scale_transforms -> Map.put(scale_transforms, aes, scales[aes].transform)
41 | end
42 | end
43 |
44 | defp fetch_fixed_aesthetics(geom_rect) do
45 | geom_rect
46 | |> Map.take([:alpha, :color, :fill, :size])
47 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
48 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
49 | end)
50 | end
51 |
52 | defp rect(row, transforms, geom_rect, plot) do
53 | mapping = geom_rect.mapping
54 |
55 | attributes = [
56 | x: transforms.x.(row[mapping.xmin]) + plot.area_padding,
57 | y: (plot.width - transforms.y.(row[mapping.ymax])) / plot.aspect_ratio + plot.area_padding,
58 | height:
59 | (transforms.y.(row[mapping.ymax]) - transforms.y.(row[geom_rect.mapping.ymin])) /
60 | plot.aspect_ratio,
61 | width: transforms.x.(row[mapping.xmax]) - transforms.x.(row[mapping.xmin]),
62 | fill_opacity: transforms.alpha.(row[geom_rect.mapping[:alpha]]),
63 | stroke: transforms.color.(row[geom_rect.mapping[:color]]),
64 | fill: transforms.fill.(row[geom_rect.mapping[:fill]]),
65 | stroke_width: transforms.size.(row[geom_rect.mapping[:size]])
66 | ]
67 |
68 | geom_rect
69 | |> GGity.Layer.custom_attributes(plot, row)
70 | |> Keyword.merge(attributes)
71 | |> Draw.rect()
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/ggity/geom/ribbon.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Ribbon do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Plot}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type record() :: map()
8 | @type mapping() :: map()
9 |
10 | defstruct data: nil,
11 | mapping: nil,
12 | stat: :identity,
13 | position: :identity,
14 | key_glyph: :rect,
15 | fill: "black",
16 | alpha: 1,
17 | color: nil,
18 | size: nil
19 |
20 | @spec new(mapping(), keyword()) :: Geom.Ribbon.t()
21 | def new(mapping, options \\ []) do
22 | struct(Geom.Ribbon, [{:mapping, mapping} | options])
23 | end
24 |
25 | @spec draw(Geom.Ribbon.t(), list(map()), Plot.t()) :: iolist()
26 | def draw(%Geom.Ribbon{} = geom_ribbon, _data, plot) do
27 | ribbons(geom_ribbon, plot)
28 | end
29 |
30 | defp ribbons(%Geom.Ribbon{position: :stack} = geom_ribbon, plot) do
31 | ribbons =
32 | geom_ribbon
33 | |> group_by_aesthetics(plot)
34 | |> Enum.sort_by(fn {value, _group} -> value end, :desc)
35 | |> Enum.map(fn {_value, group} -> ribbon(geom_ribbon, group, plot) end)
36 |
37 | plot_height = plot.width / plot.aspect_ratio
38 |
39 | ribbons
40 | |> Enum.map(fn group -> group.coords end)
41 | |> stack_coordinates(plot_height)
42 | |> Enum.zip_with(ribbons, fn stacked_coords, ribbon ->
43 | draw_ribbon(Map.put(ribbon, :coords, stacked_coords), plot.area_padding)
44 | end)
45 | end
46 |
47 | defp ribbons(%Geom.Ribbon{} = geom_ribbon, plot) do
48 | geom_ribbon
49 | |> group_by_aesthetics(plot)
50 | |> Enum.map(fn {_value, group} ->
51 | geom_ribbon
52 | |> ribbon(group, plot)
53 | |> draw_ribbon(plot.area_padding)
54 | end)
55 | end
56 |
57 | defp ribbon(%Geom.Ribbon{} = geom_ribbon, data, plot) do
58 | scale_transforms = fetch_scale_transforms(geom_ribbon.mapping, plot.scales)
59 | fixed_aesthetics = fetch_fixed_aesthetics(geom_ribbon)
60 | transforms = Map.merge(fixed_aesthetics, scale_transforms)
61 |
62 | row = hd(data)
63 | mapping = geom_ribbon.mapping
64 |
65 | [alpha, color, fill, size] = [
66 | transforms.alpha.(row[mapping[:alpha]]),
67 | transforms.color.(row[mapping[:color]]),
68 | transforms.fill.(row[mapping[:fill]]),
69 | transforms.size.(row[mapping[:size]])
70 | ]
71 |
72 | plot_height = plot.width / plot.aspect_ratio
73 | sorted_data = Enum.sort_by(data, fn row -> plot.scales.x.transform.(row[mapping[:x]]) end)
74 | y_max_coords = format_coordinates(:y_max, mapping, sorted_data, plot)
75 |
76 | all_coords =
77 | if geom_ribbon.mapping[:y_min] do
78 | y_min_coords =
79 | :y_min
80 | |> format_coordinates(mapping, sorted_data, plot)
81 | |> Enum.reverse()
82 |
83 | [y_max_coords, y_min_coords]
84 | else
85 | first_x = List.first(y_max_coords)[:x]
86 | last_x = List.last(y_max_coords)[:x]
87 | [y_max_coords, %{x: last_x, y_max: plot_height}, %{x: first_x, y_max: plot_height}]
88 | end
89 |
90 | %{fill: fill, alpha: alpha, color: color, size: size, coords: List.flatten(all_coords)}
91 | end
92 |
93 | defp fetch_scale_transforms(mapping, scales) do
94 | for aes <- [:y | Map.keys(mapping)], aes != :y_max, reduce: %{} do
95 | scale_transforms -> Map.put(scale_transforms, aes, scales[aes].transform)
96 | end
97 | end
98 |
99 | defp fetch_fixed_aesthetics(geom_ribbon) do
100 | geom_ribbon
101 | |> Map.take([:alpha, :color, :fill, :size])
102 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
103 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
104 | end)
105 | end
106 |
107 | defp group_by_aesthetics(geom, plot) do
108 | data = geom.data || plot.data
109 |
110 | Enum.group_by(data, fn row ->
111 | {
112 | row[geom.mapping[:alpha]],
113 | row[geom.mapping[:fill]]
114 | }
115 | end)
116 | end
117 |
118 | defp draw_ribbon(ribbon, area_padding) do
119 | ribbon.coords
120 | |> Enum.map_join(" ", fn row ->
121 | "#{row.x + area_padding},#{row.y_max + area_padding}"
122 | end)
123 | |> Draw.polygon(
124 | stroke: ribbon.color,
125 | stroke_width: ribbon.size,
126 | fill: ribbon.fill,
127 | fill_opacity: ribbon.alpha
128 | )
129 | end
130 |
131 | defp format_coordinates(y_aesthetic, mapping, data, plot) do
132 | Enum.map(data, fn row ->
133 | %{
134 | x: plot.scales.x.transform.(row[mapping[:x]]),
135 | y_max: transform_and_pad_y(row[mapping[y_aesthetic]], plot)
136 | }
137 | end)
138 | end
139 |
140 | defp transform_and_pad_y(y_value, plot) do
141 | (plot.width - plot.scales.y.transform.(y_value)) / plot.aspect_ratio
142 | end
143 |
144 | defp stack_coordinates([only_group | []], _plot_height), do: only_group
145 |
146 | defp stack_coordinates([first_group, next_group | rest_of_groups], plot_height) do
147 | stack_coordinates(
148 | rest_of_groups,
149 | sum_ymax_coordinates(first_group, next_group, plot_height),
150 | [first_group],
151 | plot_height
152 | )
153 | end
154 |
155 | defp stack_coordinates([], updated_group, stacked_coords, _plot_height) do
156 | [updated_group | stacked_coords]
157 | end
158 |
159 | defp stack_coordinates(
160 | [next_group | rest_of_groups],
161 | updated_group,
162 | stacked_coords,
163 | plot_height
164 | ) do
165 | stack_coordinates(
166 | rest_of_groups,
167 | sum_ymax_coordinates(next_group, updated_group, plot_height),
168 | [updated_group | stacked_coords],
169 | plot_height
170 | )
171 | end
172 |
173 | defp sum_ymax_coordinates(first_list, second_list, plot_height) do
174 | Enum.zip_with(first_list, second_list, fn first_row, second_row ->
175 | %{x: first_row.x, y_max: first_row.y_max + second_row.y_max - plot_height}
176 | end)
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/lib/ggity/geom/segment.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Segment do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Plot}
5 |
6 | @type t() :: %__MODULE__{}
7 | @type plot() :: %Plot{}
8 | @type record() :: map()
9 | @type mapping() :: map()
10 |
11 | defstruct data: nil,
12 | mapping: nil,
13 | stat: :identity,
14 | position: :identity,
15 | key_glyph: :line,
16 | alpha: 1,
17 | color: "black",
18 | size: 1,
19 | custom_attributes: nil
20 |
21 | @spec new(mapping(), keyword()) :: Geom.Segment.t()
22 | def new(mapping, options) do
23 | struct(Geom.Segment, [{:mapping, mapping} | options])
24 | end
25 |
26 | @spec draw(Geom.Segment.t(), list(map()), plot()) :: iolist()
27 | def draw(%Geom.Segment{} = geom_segment, data, plot), do: segments(geom_segment, data, plot)
28 |
29 | defp segments(%Geom.Segment{} = geom_segment, data, %Plot{scales: scales} = plot) do
30 | scale_transforms = fetch_scale_transforms(geom_segment.mapping, scales)
31 | fixed_aesthetics = fetch_fixed_aesthetics(geom_segment)
32 | all_transforms = Map.merge(fixed_aesthetics, scale_transforms)
33 |
34 | Enum.map(data, fn row -> segment(row, all_transforms, geom_segment, plot) end)
35 | end
36 |
37 | defp segment(row, transforms, geom_segment, plot) do
38 | [x, xend, y, yend, stroke_opacity, stroke, stroke_width] = [
39 | transforms.x.(row[geom_segment.mapping.x]),
40 | transforms.x.(row[geom_segment.mapping.xend]),
41 | transforms.y.(row[geom_segment.mapping.y]),
42 | transforms.y.(row[geom_segment.mapping.yend]),
43 | transforms.alpha.(row[geom_segment.mapping[:alpha]]),
44 | transforms.color.(row[geom_segment.mapping[:color]]),
45 | transforms.size.(row[geom_segment.mapping[:size]])
46 | ]
47 |
48 | x1 = x + plot.area_padding
49 | x2 = xend + plot.area_padding
50 | y1 = (plot.width - y) / plot.aspect_ratio + plot.area_padding
51 | y2 = (plot.width - yend) / plot.aspect_ratio + plot.area_padding
52 |
53 | custom_attributes = GGity.Layer.custom_attributes(geom_segment, plot, row)
54 |
55 | [
56 | x1: x1,
57 | x2: x2,
58 | y1: y1,
59 | y2: y2,
60 | stroke_opacity: stroke_opacity,
61 | stroke: stroke,
62 | stroke_width: stroke_width
63 | ]
64 | |> Keyword.merge(custom_attributes)
65 | |> Draw.line()
66 | end
67 |
68 | defp fetch_scale_transforms(mapping, scales) do
69 | for aes <- Map.keys(mapping), reduce: %{} do
70 | scale_transforms -> Map.put(scale_transforms, aes, scales[aes].transform)
71 | end
72 | end
73 |
74 | defp fetch_fixed_aesthetics(geom_segment) do
75 | geom_segment
76 | |> Map.take([:alpha, :color, :size])
77 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
78 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
79 | end)
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/ggity/geom/text.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Geom.Text do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Geom, Layer, Plot, Scale}
5 |
6 | @hjust_anchor_map %{left: "start", center: "middle", right: "end"}
7 | @vjust_anchor_map %{top: "baseline", middle: "middle", bottom: "hanging"}
8 |
9 | @type t() :: %__MODULE__{}
10 | @type plot() :: %Plot{}
11 | @type record() :: map()
12 | @type mapping() :: map()
13 |
14 | defstruct data: nil,
15 | mapping: nil,
16 | stat: :identity,
17 | position: :identity,
18 | position_vjust: 1,
19 | group_width: nil,
20 | group_padding: 5,
21 | key_glyph: :a,
22 | alpha: 1,
23 | color: "black",
24 | size: 8,
25 | family: "Helvetica, Arial, sans-serif",
26 | fontface: "normal",
27 | hjust: :center,
28 | vjust: :middle,
29 | nudge_x: 0,
30 | nudge_y: 0,
31 | custom_attributes: nil
32 |
33 | @spec new(mapping(), keyword()) :: Geom.Text.t()
34 | def new(mapping, options) do
35 | struct(Geom.Text, [{:mapping, mapping} | options])
36 | end
37 |
38 | @spec draw(Geom.Text.t(), list(map()), Plot.t()) :: iolist()
39 | def draw(%Geom.Text{} = geom_text, data, %Plot{scales: %{x: %Scale.X.Discrete{}}} = plot) do
40 | number_of_levels = length(plot.scales.x.levels)
41 | group_width = (plot.width - number_of_levels * (plot.scales.x.padding - 1)) / number_of_levels
42 | mapping = Map.new(geom_text.mapping, fn {k, v} -> {k, to_string(v)} end)
43 | geom_text = struct(geom_text, mapping: mapping, group_width: group_width)
44 | words(geom_text, data, plot)
45 | end
46 |
47 | def draw(%Geom.Text{} = geom_text, data, plot) do
48 | mapping = Map.new(geom_text.mapping, fn {k, v} -> {k, to_string(v)} end)
49 | geom_text = struct(geom_text, mapping: mapping)
50 | words(geom_text, data, plot)
51 | end
52 |
53 | defp words(%Geom.Text{} = geom_text, data, %Plot{scales: %{x: %Scale.X.Discrete{}}} = plot) do
54 | data
55 | |> Enum.reject(fn row -> row[geom_text.mapping[:y]] == 0 end)
56 | |> Enum.group_by(fn row -> row[geom_text.mapping[:x]] end)
57 | |> Enum.with_index(fn {_x_value, group}, group_index ->
58 | group(geom_text, group, group_index, plot)
59 | end)
60 | end
61 |
62 | defp words(%Geom.Text{} = geom_text, data, %Plot{scales: scales} = plot) do
63 | transforms = transforms(geom_text, scales)
64 |
65 | Enum.map(data, fn row ->
66 | row
67 | |> apply_scale_transform(transforms, geom_text.mapping)
68 | |> map_to_svg_attributes()
69 | |> draw_word(geom_text, plot)
70 | end)
71 | end
72 |
73 | defp apply_scale_transform(row, transforms, mapping) do
74 | [
75 | transforms.x.(row[mapping.x]),
76 | transforms.y.(row[mapping.y]),
77 | transforms.label.(row[mapping[:label]]),
78 | transforms.alpha.(row[mapping[:alpha]]),
79 | transforms.color.(row[mapping[:color]]),
80 | transforms.size.(row[mapping[:size]])
81 | ]
82 | end
83 |
84 | defp map_to_svg_attributes(row) do
85 | Enum.zip([:x, :y, :label, :fill_opacity, :fill, :size], row)
86 | end
87 |
88 | defp draw_word(row, geom_text, plot) do
89 | Draw.text(
90 | to_string(row[:label]),
91 | [
92 | x: row[:x] + plot.area_padding,
93 | y: (plot.width - row[:y]) / plot.aspect_ratio + plot.area_padding,
94 | fill: row[:fill],
95 | fill_opacity: row[:fill_opacity],
96 | font_size: "#{row[:size]}px",
97 | text_anchor: @hjust_anchor_map[geom_text.hjust],
98 | dominant_baseline: @vjust_anchor_map[geom_text.vjust],
99 | dx: geom_text.nudge_x,
100 | dy: -1 * geom_text.nudge_y,
101 | font_family: geom_text.family,
102 | font_weight: geom_text.fontface
103 | ] ++ Layer.custom_attributes(geom_text, plot, row)
104 | )
105 | end
106 |
107 | defp group(geom_text, group_values, group_index, %Plot{scales: scales} = plot) do
108 | transforms = transforms(geom_text, scales)
109 | count_rows = length(group_values)
110 |
111 | sort_order =
112 | case geom_text.position do
113 | :stack -> :desc
114 | :dodge -> :asc
115 | _unknown_adjustment -> :asc
116 | end
117 |
118 | group_values
119 | |> Enum.sort_by(fn row -> row[plot.mapping[:group]] end, sort_order)
120 | |> Enum.reduce({count_rows, 0, 0, []}, fn row,
121 | {number_of_groups, total_width, total_height, text} ->
122 | {
123 | number_of_groups,
124 | total_width + geom_text.group_width / count_rows,
125 | total_height +
126 | transforms.y.(row[geom_text.mapping[:y]]) / plot.aspect_ratio,
127 | [
128 | Draw.text(
129 | to_string(row[geom_text.mapping[:label]]),
130 | [
131 | x:
132 | position_adjust_x(
133 | geom_text,
134 | row,
135 | group_index,
136 | total_width,
137 | plot,
138 | number_of_groups
139 | ),
140 | y:
141 | plot.area_padding + plot.width / plot.aspect_ratio -
142 | position_adjust_y(geom_text, row, total_height, plot),
143 | fill: geom_text.color,
144 | fill_opacity: geom_text.alpha,
145 | font_size: "#{transforms.size.(row[geom_text.mapping[:size]])}pt",
146 | text_anchor: @hjust_anchor_map[geom_text.hjust],
147 | dominant_baseline: @vjust_anchor_map[geom_text.vjust],
148 | dx: geom_text.nudge_x,
149 | dy: -1 * geom_text.nudge_y,
150 | font_family: geom_text.family,
151 | font_weight: geom_text.fontface
152 | ] ++ Layer.custom_attributes(geom_text, plot, row)
153 | )
154 | | text
155 | ]
156 | }
157 | end)
158 | |> elem(3)
159 | end
160 |
161 | defp transforms(geom, scales) do
162 | scale_transforms =
163 | geom.mapping
164 | |> Map.keys()
165 | |> Enum.reduce(%{}, fn aesthetic, mapped ->
166 | Map.put(mapped, aesthetic, Map.get(scales[aesthetic], :transform))
167 | end)
168 |
169 | geom
170 | |> Map.take([:alpha, :color, :shape, :size])
171 | |> Enum.reduce(%{}, fn {aesthetic, fixed_value}, fixed ->
172 | Map.put(fixed, aesthetic, fn _value -> fixed_value end)
173 | end)
174 | |> Map.merge(scale_transforms)
175 | end
176 |
177 | defp position_adjust_x(
178 | %Geom.Text{position: :identity},
179 | row,
180 | _group_index,
181 | _total_width,
182 | plot,
183 | _number_of_groups
184 | ) do
185 | plot.scales.x.transform.(row[plot.mapping[:x]])
186 | end
187 |
188 | defp position_adjust_x(
189 | %Geom.Text{position: :stack} = geom_text,
190 | _row,
191 | group_index,
192 | _total_width,
193 | plot,
194 | _number_of_groups
195 | ) do
196 | plot.area_padding + geom_text.group_width / 2 +
197 | group_index * (geom_text.group_width + plot.scales.x.padding)
198 | end
199 |
200 | defp position_adjust_x(
201 | %Geom.Text{position: :dodge} = geom_text,
202 | _row,
203 | group_index,
204 | total_width,
205 | plot,
206 | number_of_groups
207 | ) do
208 | plot.area_padding + geom_text.group_width / 2 / number_of_groups +
209 | group_index * (geom_text.group_width + plot.scales.x.padding) +
210 | total_width
211 | end
212 |
213 | defp position_adjust_y(%Geom.Text{position: :identity} = geom_text, row, _total_height, plot) do
214 | plot.scales.y.transform.(row[geom_text.mapping[:y]]) / plot.aspect_ratio
215 | end
216 |
217 | defp position_adjust_y(%Geom.Text{position: :stack} = geom_text, row, total_height, plot) do
218 | total_height +
219 | plot.scales.y.transform.(row[geom_text.mapping[:y]]) / plot.aspect_ratio *
220 | geom_text.position_vjust
221 | end
222 |
223 | defp position_adjust_y(%Geom.Text{position: :dodge} = geom_text, row, _total_height, plot) do
224 | plot.scales.y.transform.(row[geom_text.mapping[:y]]) / plot.aspect_ratio *
225 | geom_text.position_vjust
226 | end
227 | end
228 |
--------------------------------------------------------------------------------
/lib/ggity/html.ex:
--------------------------------------------------------------------------------
1 | # Copied directly from the Plug.HTML module in the Plug library.
2 |
3 | # Copyright (c) 2013 Plataformatec. Used under the terms of the
4 | # applicable license - notice below.
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | defmodule GGity.HTML do
19 | @moduledoc false
20 |
21 | @doc false
22 | @spec escape(String.t()) :: String.t()
23 | def escape(data) when is_binary(data) do
24 | IO.iodata_to_binary(to_iodata(data, 0, data, []))
25 | end
26 |
27 | @doc false
28 | @spec escape_to_iodata(String.t()) :: iodata
29 | def escape_to_iodata(data) when is_binary(data) do
30 | to_iodata(data, 0, data, [])
31 | end
32 |
33 | escapes = [
34 | {?<, "<"},
35 | {?>, ">"},
36 | {?&, "&"},
37 | {?", """},
38 | {?', "'"}
39 | ]
40 |
41 | for {match, insert} <- escapes do
42 | defp to_iodata(<>, skip, original, acc) do
43 | to_iodata(rest, skip + 1, original, [acc | unquote(insert)])
44 | end
45 | end
46 |
47 | defp to_iodata(<<_char, rest::bits>>, skip, original, acc) do
48 | to_iodata(rest, skip, original, acc, 1)
49 | end
50 |
51 | defp to_iodata(<<>>, _skip, _original, acc) do
52 | acc
53 | end
54 |
55 | for {match, insert} <- escapes do
56 | defp to_iodata(<>, skip, original, acc, len) do
57 | part = binary_part(original, skip, len)
58 | to_iodata(rest, skip + len + 1, original, [acc, part | unquote(insert)])
59 | end
60 | end
61 |
62 | defp to_iodata(<<_char, rest::bits>>, skip, original, acc, len) do
63 | to_iodata(rest, skip, original, acc, len + 1)
64 | end
65 |
66 | defp to_iodata(<<>>, 0, original, _acc, _len) do
67 | original
68 | end
69 |
70 | defp to_iodata(<<>>, skip, original, acc, len) do
71 | [acc | binary_part(original, skip, len)]
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/ggity/labels.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Labels do
2 | @moduledoc """
3 | Common functions for transforming axis tick labels.
4 |
5 | Break (i.e. axis tick or legend item) labels are formatted based on a
6 | scale's `:labels` option. This option can be provided in several forms:
7 |
8 | - `nil` - No labels are drawn
9 | - `:waivers` - drawn using the default formatting (`Kernel.to_string/1`)
10 | - a function that takes a single argument representing the break value and returns a binary
11 | - an atom, representing the name of a built-in formatting function (e.g., `commas/1`)
12 |
13 | Note that the built-in formatting functions are not intended to be robust. One option for
14 | finely-tuned formatting would be to pass functions from the [Cldr family of packages](https://hexdocs.pm/ex_cldr) (must be
15 | added as a separate dependency).
16 | ## Examples
17 |
18 | ```
19 | data
20 | |> Plot.new(%{x: "x", y: "y"})
21 | |> Plot.geom_point()
22 | |> Plot.scale_x_continuous(labels: nil)
23 | # value 1000 is printed as an empty string
24 | ```
25 |
26 | ```
27 | data
28 | |> Plot.new(%{x: "x", y: "y"})
29 | |> Plot.geom_point()
30 | |> Plot.scale_x_continuous() # This is equivalent to Plot.scale_x_continuous(labels: :waivers)
31 | # value 1000 (integer) is printed as "1000"
32 | # value 1000.0 (float) is printed as "1.0e3"
33 |
34 | ```
35 |
36 | ```
37 | data
38 | |> Plot.new(%{x: "x", y: "y", c: "color"})
39 | |> Plot.geom_point()
40 | |> Plot.scale_color_viridis(labels: fn value -> value <> "!" end)
41 | # value "First Item" is printed as "First Item!"
42 | ```
43 |
44 | ```
45 | data
46 | |> Plot.new(%{x: "x", y: "y"})
47 | |> Plot.geom_point()
48 | |> Plot.scale_x_continuous(labels: :commas)
49 | # value 1000 (integer) is printed as "1,000"
50 | # value 1000 (float) is printed as "1,000"
51 | ```
52 |
53 | Date scales (e.g., `GGity.Scale.X.Date`) are a special case.
54 | For those scales, if a value for `:date_labels` has been specified, that
55 | pattern overrides any value for the `:labels` option. See
56 | `GGity.Plot.scale_x_date/2` for more information regarding
57 | date labels.
58 | """
59 |
60 | alias GGity.{Labels, Scale}
61 |
62 | @type tick_value() :: %Date{} | %DateTime{} | number()
63 |
64 | @doc false
65 | @spec format(map(), tick_value()) :: String.t()
66 | def format(%scale_type{date_labels: {pattern, options}}, value)
67 | when scale_type in [Scale.X.Date, Scale.X.DateTime] do
68 | NimbleStrftime.format(value, pattern, options)
69 | end
70 |
71 | def format(%scale_type{date_labels: pattern}, value)
72 | when scale_type in [Scale.X.Date, Scale.X.DateTime] and is_binary(pattern) do
73 | NimbleStrftime.format(value, pattern)
74 | end
75 |
76 | def format(%{labels: :waivers}, value), do: to_string(value)
77 |
78 | def format(%{labels: nil}, _value), do: ""
79 |
80 | def format(%{labels: built_in_function}, value) when is_atom(built_in_function) do
81 | apply(Labels, built_in_function, [value])
82 | end
83 |
84 | def format(%{labels: formatter}, value) when is_function(formatter) do
85 | formatter.(value)
86 | end
87 |
88 | @doc """
89 | Applies a comma separator to a number and converts it to a string.
90 |
91 | If the number is a float, it is first rounded using `Kernel.to_string/1`.
92 |
93 | Note that simple floating point arithmetic is used; the various issues/errors
94 | associated with floating point values apply.
95 |
96 | iex> GGity.Labels.commas(5000.0)
97 | "5,000"
98 |
99 | iex> GGity.Labels.commas(1000.6)
100 | "1,001"
101 |
102 | iex> GGity.Labels.commas(100.0)
103 | "100"
104 |
105 | iex> GGity.Labels.commas(10_000_000)
106 | "10,000,000"
107 | """
108 | @spec commas(number()) :: String.t()
109 | def commas(value) when is_number(value) do
110 | value
111 | |> round()
112 | |> to_charlist()
113 | |> Enum.reverse()
114 | |> comma_separate([])
115 | |> to_string()
116 | end
117 |
118 | defp comma_separate([first, second, third | []], acc) do
119 | [third, second, first | acc]
120 | end
121 |
122 | defp comma_separate([first, second, third | tail], acc) do
123 | acc = [~c",", third, second, first | acc]
124 | comma_separate(tail, acc)
125 | end
126 |
127 | defp comma_separate([first | [second]], acc), do: [second, first | acc]
128 | defp comma_separate([first], acc), do: [first | acc]
129 | defp comma_separate([], acc), do: acc
130 |
131 | @doc """
132 | Formats a number in U.S. dollars and cents.
133 |
134 | If the value is greater than or equal to 100,000 rounds to the nearest
135 | dollar and does not display cents.
136 |
137 | Note that simple floating point arithmetic is used; the various issues/errors
138 | associated with floating point values apply.
139 |
140 | iex> GGity.Labels.dollar(5000.0)
141 | "$5,000.00"
142 |
143 | iex> GGity.Labels.dollar(1000.6)
144 | "$1,000.60"
145 |
146 | iex> GGity.Labels.dollar(100.0)
147 | "$100.00"
148 |
149 | iex> GGity.Labels.dollar(10_000_000)
150 | "$10,000,000"
151 | """
152 | @spec dollar(number()) :: String.t()
153 | def dollar(value) when is_float(value) do
154 | cents =
155 | ((Float.round(value, 2) - floor(value)) * 100)
156 | |> round()
157 | |> Integer.digits()
158 | |> Enum.take(2)
159 | |> Enum.join()
160 | |> String.pad_trailing(2, "0")
161 |
162 | case value do
163 | value when value >= 100_000 ->
164 | "$" <> commas(floor(value))
165 |
166 | value ->
167 | "$#{commas(floor(value))}.#{cents}"
168 | end
169 | end
170 |
171 | def dollar(value) when is_integer(value) do
172 | dollar(value * 1.0)
173 | end
174 |
175 | @doc """
176 | Formats a number as a percent.
177 |
178 | Accepts a `:precision` option specifying the number of decimal places
179 | to be displayed.
180 |
181 | Note that simple floating point arithmetic is used; the various issues/errors
182 | associated with floating point values apply.
183 |
184 | iex> GGity.Labels.percent(0.5)
185 | "50%"
186 |
187 | iex> GGity.Labels.percent(0.111)
188 | "11%"
189 |
190 | iex> GGity.Labels.percent(0.015, precision: 1)
191 | "1.5%"
192 |
193 | iex> GGity.Labels.percent(10)
194 | "1000%"
195 | """
196 | @spec percent(number(), keyword()) :: String.t()
197 | def percent(value, options \\ [precision: 0])
198 |
199 | def percent(value, precision: precision) when is_float(value) do
200 | percent_value = Float.round(value * 100, precision)
201 |
202 | rounded_value =
203 | case precision do
204 | 0 -> round(percent_value)
205 | _other -> percent_value
206 | end
207 |
208 | to_string(rounded_value) <> "%"
209 | end
210 |
211 | def percent(value, _options) when is_integer(value) do
212 | to_string(value * 100) <> "%"
213 | end
214 | end
215 |
--------------------------------------------------------------------------------
/lib/ggity/layer.ex:
--------------------------------------------------------------------------------
1 | defprotocol GGity.Layer do
2 | @moduledoc false
3 |
4 | @spec new(GGity.Layer.t(), map(), keyword()) :: GGity.Layer.t()
5 | def new(geom, mapping, options)
6 |
7 | @spec draw(GGity.Layer.t(), list(map()), GGity.Plot.t()) :: iolist()
8 | def draw(geom, data, plot)
9 |
10 | @spec custom_attributes(GGity.Layer.t(), GGity.Plot.t(), map()) :: keyword()
11 | def custom_attributes(geom, plot, row)
12 | end
13 |
14 | defimpl GGity.Layer,
15 | for: [
16 | GGity.Geom.Bar,
17 | GGity.Geom.Boxplot,
18 | GGity.Geom.Line,
19 | GGity.Geom.Point,
20 | GGity.Geom.Rect,
21 | GGity.Geom.Ribbon,
22 | GGity.Geom.Segment,
23 | GGity.Geom.Text
24 | ] do
25 | def new(%geom_type{} = _geom, mapping, options) do
26 | apply(geom_type, :new, [mapping, options])
27 | end
28 |
29 | def draw(%geom_type{} = geom, data, plot) do
30 | data = Explorer.DataFrame.to_rows(data)
31 | geom = %{geom | data: Explorer.DataFrame.to_rows(geom.data)}
32 |
33 | apply(geom_type, :draw, [geom, data, plot])
34 | end
35 |
36 | def custom_attributes(%{custom_attributes: nil}, _plot, _row), do: []
37 |
38 | def custom_attributes(geom, plot, row) do
39 | geom.custom_attributes.(plot, row)
40 | end
41 | end
42 |
43 | defimpl GGity.Layer, for: GGity.Geom.Blank do
44 | def new(_geom, _mapping, _options), do: struct(GGity.Geom.Blank)
45 |
46 | def draw(_geom, _data, _plot), do: GGity.Geom.Blank.draw()
47 |
48 | def custom_attributes(_geom, _data, _plot), do: []
49 | end
50 |
--------------------------------------------------------------------------------
/lib/ggity/legend.ex:
--------------------------------------------------------------------------------
1 | defprotocol GGity.Legend do
2 | @moduledoc false
3 |
4 | @fallback_to_any true
5 |
6 | @spec draw_legend(GGity.Legend.t(), binary(), atom(), number(), keyword()) :: iolist()
7 | def draw_legend(scale, label, key_glyph, key_height, fixed_aesthetics)
8 | end
9 |
10 | defimpl GGity.Legend,
11 | for: [
12 | GGity.Scale.Alpha.Discrete,
13 | GGity.Scale.Color.Viridis,
14 | GGity.Scale.Fill.Viridis,
15 | GGity.Scale.Linetype.Discrete,
16 | GGity.Scale.Size
17 | ] do
18 | def draw_legend(%scale_type{} = scale, label, key_glyph, key_height, fixed_aesthetics) do
19 | apply(scale_type, :draw_legend, [scale, label, key_glyph, key_height, fixed_aesthetics])
20 | end
21 | end
22 |
23 | defimpl GGity.Legend, for: [GGity.Scale.Shape, GGity.Scale.Shape.Manual] do
24 | def draw_legend(%scale_type{} = scale, label, _key_glyph, key_height, fixed_aesthetics) do
25 | apply(scale_type, :draw_legend, [scale, label, key_height, fixed_aesthetics])
26 | end
27 | end
28 |
29 | defimpl GGity.Legend, for: Any do
30 | def draw_legend(_scale, _label, _key_glyph, _key_height, _fixed_aesthetics), do: []
31 | end
32 |
--------------------------------------------------------------------------------
/lib/ggity/scale.ex:
--------------------------------------------------------------------------------
1 | defprotocol GGity.Scale do
2 | @moduledoc false
3 |
4 | @type date() :: %Date{}
5 | @type datetime() :: %DateTime{} | %NaiveDateTime{}
6 | @type date_or_time() :: date() | datetime()
7 | @type continuous_value :: number() | date_or_time()
8 |
9 | @spec train(GGity.Scale.t(), {continuous_value(), continuous_value()} | list(binary())) ::
10 | GGity.Scale.t()
11 | def train(scale, parameters)
12 | end
13 |
14 | defimpl GGity.Scale,
15 | for: [
16 | GGity.Scale.Alpha.Discrete,
17 | GGity.Scale.Color.Viridis,
18 | GGity.Scale.Fill.Viridis,
19 | GGity.Scale.Identity,
20 | GGity.Scale.Linetype.Discrete,
21 | GGity.Scale.Shape.Manual,
22 | GGity.Scale.Shape,
23 | GGity.Scale.X.Discrete
24 | ] do
25 | def train(%scale_type{} = scale, levels) do
26 | apply(scale_type, :train, [scale, levels])
27 | end
28 | end
29 |
30 | defimpl GGity.Scale,
31 | for: [
32 | GGity.Scale.Alpha.Continuous,
33 | GGity.Scale.Size,
34 | GGity.Scale.X.Continuous,
35 | GGity.Scale.X.Date,
36 | GGity.Scale.X.DateTime,
37 | GGity.Scale.Y.Continuous
38 | ] do
39 | def train(%scale_type{} = scale, {min, max}) do
40 | apply(scale_type, :train, [scale, {min, max}])
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/ggity/scale/alpha_continuous.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Alpha.Continuous do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.Alpha
5 |
6 | @type t() :: %__MODULE__{}
7 |
8 | defstruct range: {0.1, 1},
9 | transform: nil
10 |
11 | @spec new(keyword()) :: Alpha.Continuous.t()
12 | def new(options \\ []), do: struct(Alpha.Continuous, options)
13 |
14 | @spec train(Alpha.Continuous.t(), {number(), number()}) :: Alpha.Continuous.t()
15 | def train(scale, {value_min, value_max}) do
16 | domain = value_max - value_min
17 | transformations(scale, domain, value_min)
18 | end
19 |
20 | defp transformations(scale, 0, _value_min) do
21 | struct(scale, transform: fn _value -> 1 end)
22 | end
23 |
24 | defp transformations(scale, domain, value_min) do
25 | {range_min, range_max} = scale.range
26 | range = range_max - range_min
27 |
28 | struct(scale,
29 | transform: fn value -> range_min + (value - value_min) / domain * range end
30 | )
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/ggity/scale/alpha_discrete.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Alpha.Discrete do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Labels}
5 | alias GGity.Scale.Alpha
6 |
7 | defstruct transform: nil,
8 | range: {0.1, 1},
9 | levels: nil,
10 | labels: :waivers,
11 | guide: :legend
12 |
13 | @type t() :: %__MODULE__{}
14 |
15 | @spec new(keyword()) :: Alpha.Discrete.t()
16 | def new(options \\ []), do: struct(Alpha.Discrete, options)
17 |
18 | @spec train(Alpha.Discrete.t(), list(binary())) :: Alpha.Discrete.t()
19 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
20 | transform = GGity.Scale.Discrete.transform(levels, palette(scale, levels))
21 | struct(scale, levels: levels, transform: transform)
22 | end
23 |
24 | defp palette(%Alpha.Discrete{range: {min, max}}, [_single_level]) do
25 | [min + (max - min) / 2]
26 | end
27 |
28 | defp palette(%Alpha.Discrete{range: {min, max}}, levels) do
29 | number_of_levels = length(levels)
30 | interval = (max - min) / (number_of_levels - 1)
31 | for index <- 1..number_of_levels, do: min + (index - 1) * interval
32 | end
33 |
34 | @spec draw_legend(Alpha.Discrete.t(), binary(), atom(), number(), keyword()) :: iolist()
35 | def draw_legend(
36 | %Alpha.Discrete{guide: :none},
37 | _label,
38 | _key_glyph,
39 | _key_height,
40 | _fixed_aesthetics
41 | ),
42 | do: []
43 |
44 | def draw_legend(
45 | %Alpha.Discrete{levels: [_]},
46 | _label,
47 | _key_glyph,
48 | _key_height,
49 | _fixed_aethetics
50 | ),
51 | do: []
52 |
53 | def draw_legend(
54 | %Alpha.Discrete{levels: levels} = scale,
55 | label,
56 | key_glyph,
57 | key_height,
58 | fixed_aethetics
59 | ) do
60 | [
61 | Draw.text(
62 | "#{label}",
63 | x: "0",
64 | y: "-5",
65 | class: "gg-text gg-legend-title",
66 | text_anchor: "left"
67 | ),
68 | Enum.with_index(levels, fn level, index ->
69 | draw_legend_item(scale, {level, index}, key_glyph, key_height, fixed_aethetics)
70 | end)
71 | ]
72 | end
73 |
74 | defp draw_legend_item(scale, {level, index}, key_glyph, key_height, fixed_aesthetics) do
75 | [
76 | Draw.rect(
77 | x: "0",
78 | y: "#{key_height * index}",
79 | height: key_height,
80 | width: key_height,
81 | class: "gg-legend-key"
82 | ),
83 | draw_key_glyph(scale, level, index, key_glyph, key_height, fixed_aesthetics),
84 | Draw.text(
85 | "#{Labels.format(scale, level)}",
86 | x: "#{key_height + 5}",
87 | y: "#{10 + key_height * index}",
88 | class: "gg-text gg-legend-text",
89 | text_anchor: "left"
90 | )
91 | ]
92 | end
93 |
94 | defp draw_key_glyph(scale, level, index, :rect, key_height, fixed_aesthetics) do
95 | Draw.rect(
96 | x: "0",
97 | y: "#{key_height * index}",
98 | height: key_height,
99 | width: key_height,
100 | fill_opacity: "#{scale.transform.(level)}",
101 | fill: fixed_aesthetics[:fill]
102 | )
103 | end
104 |
105 | defp draw_key_glyph(scale, level, index, :a, key_height, fixed_aesthetics) do
106 | Draw.text(
107 | "a",
108 | x: key_height / 2,
109 | y: key_height / 2 + key_height * index,
110 | text_anchor: "middle",
111 | dominant_baseline: "middle",
112 | font_size: 10,
113 | font_weight: "bold",
114 | fill: fixed_aesthetics[:color],
115 | fill_opacity: "#{scale.transform.(level)}"
116 | )
117 | end
118 |
119 | defp draw_key_glyph(scale, level, index, :point, key_height, fixed_aesthetics) do
120 | GGity.Shapes.draw(
121 | :circle,
122 | {key_height / 2, key_height / 2 + key_height * index},
123 | :math.pow(1 + key_height / 3, 2),
124 | color: fixed_aesthetics[:color],
125 | fill: fixed_aesthetics[:fill],
126 | fill_opacity: "#{scale.transform.(level)}"
127 | )
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/lib/ggity/scale/alpha_manual.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Alpha.Manual do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.Alpha
5 |
6 | @default_alpha 1
7 |
8 | @type t() :: %__MODULE__{}
9 |
10 | defstruct transform: nil
11 |
12 | @doc false
13 | @spec new(any()) :: Alpha.Manual.t()
14 | def new(value \\ @default_alpha) when value >= 0 and value <= 1 do
15 | struct(Alpha.Manual, transform: fn _value -> value end)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/ggity/scale/color_manual.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Color.Manual do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.Color
5 |
6 | @default_color "black"
7 |
8 | @type t() :: %__MODULE__{}
9 |
10 | defstruct transform: nil,
11 | levels: nil
12 |
13 | @spec new(any()) :: Color.Manual.t()
14 | def new(value \\ @default_color) when is_binary(value) do
15 | struct(Color.Manual, levels: [value], transform: fn _value -> value end)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/ggity/scale/continuous.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Continuous do
2 | @moduledoc false
3 |
4 | @type extent() :: number() | %Date{} | %DateTime{} | %NaiveDateTime{}
5 |
6 | @doc false
7 | @spec transform({extent(), extent()}, {extent(), extent()}) :: (extent() -> number())
8 | def transform({domain_min, domain_max}, {range_min, range_max}) do
9 | fn value ->
10 | diff(value, domain_min) / diff(domain_max, domain_min) * diff(range_max, range_min)
11 | end
12 | end
13 |
14 | defp diff(first, second) when is_number(first) and is_number(second) do
15 | first - second
16 | end
17 |
18 | defp diff(%date_type{} = first, %date_type{} = second) when date_type == Date do
19 | Date.diff(first, second)
20 | end
21 |
22 | defp diff(%date_type{} = first, %date_type{} = second)
23 | when date_type in [DateTime, NaiveDateTime] do
24 | apply(date_type, :diff, [first, second, :millisecond])
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/ggity/scale/discrete.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Discrete do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec transform(list(), list()) :: (binary() -> binary())
6 | def transform(levels, palette) when is_list(levels) and is_list(palette) do
7 | levels_map =
8 | palette
9 | |> Enum.zip(levels)
10 | |> Map.new(fn {aesthetic, value} -> {value, aesthetic} end)
11 |
12 | fn value -> levels_map[to_string(value)] end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/ggity/scale/fill_viridis.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Fill.Viridis do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Labels}
5 | alias GGity.Scale.{Color, Fill}
6 |
7 | defstruct transform: nil,
8 | levels: nil,
9 | labels: :waivers,
10 | guide: :legend,
11 | option: :viridis
12 |
13 | @type t() :: %__MODULE__{}
14 |
15 | @spec new(keyword()) :: Fill.Viridis.t()
16 | def new(options \\ []), do: struct(Fill.Viridis, options)
17 |
18 | @spec train(Fill.Viridis.t(), list(binary())) :: Fill.Viridis.t()
19 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
20 | color_struct =
21 | Color.Viridis
22 | |> struct(Map.from_struct(scale))
23 | |> Color.Viridis.train(levels)
24 |
25 | struct(Fill.Viridis, Map.from_struct(color_struct))
26 | end
27 |
28 | @spec draw_legend(Fill.Viridis.t(), binary(), atom(), number(), keyword()) :: iolist()
29 | def draw_legend(
30 | %Fill.Viridis{guide: :none},
31 | _label,
32 | _key_glyph,
33 | _key_height,
34 | _fixed_aesthetics
35 | ),
36 | do: []
37 |
38 | def draw_legend(%Fill.Viridis{levels: [_]}, _label, _key_glyp, _key_heighth, _fixed_aesthetics),
39 | do: []
40 |
41 | def draw_legend(
42 | %Fill.Viridis{levels: levels} = scale,
43 | label,
44 | key_glyph,
45 | key_height,
46 | fixed_aesthetics
47 | ) do
48 | [
49 | Draw.text(
50 | "#{label}",
51 | x: "0",
52 | y: "-5",
53 | class: "gg-text gg-legend-title",
54 | text_anchor: "left"
55 | ),
56 | Enum.with_index(levels, fn level, index ->
57 | draw_legend_item(scale, {level, index}, key_glyph, key_height, fixed_aesthetics)
58 | end)
59 | ]
60 | end
61 |
62 | defp draw_legend_item(scale, {level, index}, key_glyph, key_height, fixed_aesthetics) do
63 | [
64 | draw_key_glyph(scale, level, index, key_glyph, key_height, fixed_aesthetics),
65 | Draw.text(
66 | "#{Labels.format(scale, level)}",
67 | x: "#{5 + key_height}",
68 | y: "#{10 + key_height * index}",
69 | class: "gg-text gg-legend-text",
70 | text_anchor: "left"
71 | )
72 | ]
73 | end
74 |
75 | defp draw_key_glyph(scale, level, index, :rect, key_height, fixed_aesthetics) do
76 | Draw.rect(
77 | x: "0",
78 | y: "#{key_height * index}",
79 | height: key_height,
80 | width: key_height,
81 | style: "fill:#{scale.transform.(level)}; fill-opacity:#{fixed_aesthetics[:alpha]};",
82 | class: "gg-legend-key"
83 | )
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/ggity/scale/identity.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Identity do
2 | @moduledoc false
3 |
4 | alias GGity.Scale
5 |
6 | @type t() :: %__MODULE__{}
7 |
8 | defstruct transform: nil,
9 | levels: nil,
10 | labels: :waivers,
11 | guide: :none,
12 | aesthetic: nil
13 |
14 | @spec new(atom()) :: Scale.Identity.t()
15 | def new(aesthetic) do
16 | struct(Scale.Identity, aesthetic: aesthetic)
17 | end
18 |
19 | @spec train(Scale.Identity.t(), list(binary())) :: Scale.Identity.t()
20 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
21 | transform = fn value -> value end
22 | struct(scale, levels: levels, transform: transform)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/ggity/scale/linetype_discrete.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Linetype.Discrete do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Labels}
5 | alias GGity.Scale.Linetype
6 |
7 | # solid: "",
8 | # dashed: "4",
9 | # dotted: "1",
10 | # longdash: "6 2",
11 | # dotdash: "1 2 3 2",
12 | # twodash: "2 2 6 2"
13 |
14 | @palette_values [
15 | "",
16 | "4",
17 | "1",
18 | "6 2",
19 | "1 2 3 2",
20 | "2 2 6 2"
21 | ]
22 |
23 | defstruct transform: nil,
24 | levels: nil,
25 | labels: :waivers,
26 | guide: :legend
27 |
28 | @type t() :: %__MODULE__{}
29 |
30 | @spec new(keyword()) :: Linetype.Discrete.t()
31 | def new(options \\ []), do: struct(Linetype.Discrete, options)
32 |
33 | @spec train(Linetype.Discrete.t(), list(binary())) :: Linetype.Discrete.t()
34 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
35 | transform = GGity.Scale.Discrete.transform(levels, palette(levels))
36 | struct(scale, levels: levels, transform: transform)
37 | end
38 |
39 | defp palette(levels) do
40 | @palette_values
41 | |> Stream.cycle()
42 | |> Enum.take(length(levels))
43 | end
44 |
45 | @spec draw_legend(Linetype.Discrete.t(), binary(), atom(), number(), keyword()) :: iolist()
46 | def draw_legend(
47 | %Linetype.Discrete{guide: :none},
48 | _label,
49 | _key_glyph,
50 | _key_height,
51 | _fixed_aesthetics
52 | ),
53 | do: []
54 |
55 | def draw_legend(
56 | %Linetype.Discrete{levels: [_]},
57 | _label,
58 | _key_glyph,
59 | _key_height,
60 | _fixed_aesthetics
61 | ),
62 | do: []
63 |
64 | def draw_legend(
65 | %Linetype.Discrete{levels: levels} = scale,
66 | label,
67 | key_glyph,
68 | key_height,
69 | fixed_aesthetics
70 | ) do
71 | [
72 | Draw.text(
73 | "#{label}",
74 | x: "0",
75 | y: "-5",
76 | class: "gg-text gg-legend-title",
77 | text_anchor: "left"
78 | ),
79 | Enum.with_index(levels, fn level, index ->
80 | draw_legend_item(scale, {level, index}, key_glyph, key_height, fixed_aesthetics)
81 | end)
82 | ]
83 | end
84 |
85 | defp draw_legend_item(scale, {level, index}, key_glyph, key_height, fixed_aesthetics) do
86 | [
87 | Draw.rect(
88 | x: "0",
89 | y: "#{key_height * index}",
90 | height: key_height,
91 | width: key_height,
92 | class: "gg-legend-key"
93 | ),
94 | draw_key_glyph(scale, level, index, key_glyph, key_height, fixed_aesthetics),
95 | Draw.text(
96 | "#{Labels.format(scale, level)}",
97 | x: "#{key_height + 5}",
98 | y: "#{10 + key_height * index}",
99 | class: "gg-text gg-legend-text",
100 | text_anchor: "left"
101 | )
102 | ]
103 | end
104 |
105 | defp draw_key_glyph(scale, level, index, :path, key_height, fixed_aesthetics) do
106 | Draw.line(
107 | x1: 1,
108 | y1: key_height / 2 + key_height * index,
109 | x2: key_height - 1,
110 | y2: key_height / 2 + key_height * index,
111 | stroke: fixed_aesthetics[:color],
112 | stroke_dasharray: "#{scale.transform.(level)}",
113 | stroke_opacity: fixed_aesthetics[:alpha]
114 | )
115 | end
116 |
117 | defp draw_key_glyph(scale, level, index, :timeseries, key_height, fixed_aesthetics) do
118 | offset = key_height * index
119 |
120 | Draw.polyline(
121 | [
122 | {1, key_height - 1 + offset},
123 | {key_height / 5 * 2, key_height / 5 * 2 + offset},
124 | {key_height / 5 * 3, key_height / 5 * 3 + offset},
125 | {key_height - 1, 1 + offset}
126 | ],
127 | fixed_aesthetics[:color],
128 | fixed_aesthetics[:size],
129 | fixed_aesthetics[:linetype],
130 | scale.transform.(level)
131 | )
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/lib/ggity/scale/linetype_manual.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Linetype.Manual do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.Linetype
5 |
6 | @default_linetype :solid
7 | @linetype_specs %{
8 | solid: "",
9 | dashed: "4",
10 | dotted: "1",
11 | longdash: "6 2",
12 | dotdash: "1 2 3 2",
13 | twodash: "2 2 6 2"
14 | }
15 | @valid_linetypes Map.keys(@linetype_specs)
16 |
17 | @type t() :: %__MODULE__{}
18 |
19 | defstruct transform: nil,
20 | levels: nil
21 |
22 | @spec new(atom()) :: Linetype.Manual.t()
23 | def new(value \\ @default_linetype) when value in @valid_linetypes do
24 | value = get_specs(value)
25 | struct(Linetype.Manual, levels: [value], transform: fn _value -> value end)
26 | end
27 |
28 | defp get_specs(value) do
29 | @linetype_specs[value] || ""
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/ggity/scale/shape.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Shape do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Labels}
5 | alias GGity.Scale.Shape
6 |
7 | @palette_values [:circle, :triangle, :square, :plus, :square_cross]
8 |
9 | defstruct transform: nil,
10 | levels: nil,
11 | labels: :waivers,
12 | guide: :legend
13 |
14 | @type t() :: %__MODULE__{}
15 |
16 | @spec new(keyword()) :: Shape.t()
17 | def new(options \\ []), do: struct(Shape, options)
18 |
19 | @spec train(Shape.t(), list(binary())) :: Shape.t()
20 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
21 | transform = GGity.Scale.Discrete.transform(levels, palette(levels))
22 | struct(scale, levels: levels, transform: transform)
23 | end
24 |
25 | defp palette(levels) do
26 | @palette_values
27 | |> Stream.cycle()
28 | |> Enum.take(length(levels))
29 | end
30 |
31 | @spec draw_legend(Shape.t(), binary(), number(), keyword()) :: iolist()
32 | def draw_legend(%Shape{guide: :none}, _label, _key_height, _fixed_aesthetics), do: []
33 |
34 | def draw_legend(%Shape{levels: [_]}, _label, _key_height, _fixed_aesthetics), do: []
35 |
36 | def draw_legend(%Shape{levels: levels} = scale, label, key_height, fixed_aesthetics) do
37 | [
38 | Draw.text(
39 | "#{label}",
40 | x: "0",
41 | y: "-5",
42 | class: "gg-text gg-legend-title",
43 | text_anchor: "left"
44 | ),
45 | Enum.with_index(levels, fn level, index ->
46 | draw_legend_item(scale, {level, index}, key_height, fixed_aesthetics)
47 | end)
48 | ]
49 | end
50 |
51 | defp draw_legend_item(scale, {level, index}, key_height, fixed_aesthetics) do
52 | transformed_value = scale.transform.(level)
53 |
54 | [
55 | Draw.rect(
56 | x: "0",
57 | y: "#{key_height * index}",
58 | height: key_height,
59 | width: key_height,
60 | class: "gg-legend-key"
61 | ),
62 | GGity.Shapes.draw(
63 | transformed_value,
64 | {key_height / 2, key_height / 2 + key_height * index},
65 | :math.pow(1 + key_height / 3, 2),
66 | fill: fixed_aesthetics[:fill],
67 | color: fixed_aesthetics[:color],
68 | fill_opacity: fixed_aesthetics[:alpha]
69 | ),
70 | Draw.text(
71 | "#{Labels.format(scale, level)}",
72 | x: "#{5 + key_height}",
73 | y: "#{10 + key_height * index}",
74 | class: "gg-text gg-legend-text",
75 | text_anchor: "left"
76 | )
77 | ]
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/ggity/scale/shape_manual.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Shape.Manual do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Labels}
5 | alias GGity.Scale.Shape
6 |
7 | @type t() :: %__MODULE__{}
8 |
9 | defstruct levels: nil,
10 | transform: nil,
11 | labels: :waivers,
12 | guide: :legend,
13 | values: []
14 |
15 | @spec new(keyword()) :: Shape.Manual.t()
16 | def new(options) do
17 | values =
18 | options
19 | |> Keyword.get(:values)
20 | |> set_values()
21 |
22 | options = Keyword.put_new(options, :values, values)
23 | struct(Shape.Manual, options)
24 | end
25 |
26 | defp set_values(nil),
27 | do: raise(ArgumentError, "Manual scales must be passed a :values option with scale values.")
28 |
29 | defp set_values([value | _other_values] = values) when is_binary(value) do
30 | values
31 | end
32 |
33 | @spec train(Shape.Manual.t(), list(binary())) :: Shape.Manual.t()
34 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
35 | number_of_levels = length(levels)
36 |
37 | palette =
38 | scale.values
39 | |> Stream.cycle()
40 | |> Enum.take(number_of_levels)
41 | |> List.to_tuple()
42 |
43 | values_map =
44 | levels
45 | |> Enum.with_index(fn level, index ->
46 | {level, elem(palette, index)}
47 | end)
48 | |> Map.new()
49 |
50 | struct(scale, levels: levels, transform: fn value -> values_map[to_string(value)] end)
51 | end
52 |
53 | @spec draw_legend(Shape.Manual.t(), binary(), number(), keyword()) :: iolist()
54 | def draw_legend(%Shape.Manual{guide: :none}, _label, _key_height, _fixed_aesthetics), do: []
55 |
56 | def draw_legend(%Shape.Manual{levels: []}, _label, _key_height, _fixed_aesthetics), do: []
57 |
58 | def draw_legend(%Shape.Manual{levels: [_]}, _label, _key_height, _fixed_aesthetics), do: []
59 |
60 | def draw_legend(%Shape.Manual{levels: levels} = scale, label, key_height, fixed_aesthetics) do
61 | [
62 | Draw.text(
63 | "#{label}",
64 | x: "0",
65 | y: "-5",
66 | class: "gg-text gg-legend-title",
67 | text_anchor: "left"
68 | ),
69 | Enum.with_index(levels, fn level, index ->
70 | draw_legend_item(scale, {level, index}, key_height, fixed_aesthetics)
71 | end)
72 | ]
73 | end
74 |
75 | defp draw_legend_item(scale, {level, index}, key_height, fixed_aesthetics) do
76 | marker = scale.transform.(level)
77 |
78 | size =
79 | case marker do
80 | character when is_binary(character) -> 7 / 15 * key_height
81 | _otherwise -> key_height / 3
82 | end
83 |
84 | [
85 | Draw.rect(
86 | x: "0",
87 | y: "#{key_height * index}",
88 | height: key_height,
89 | width: key_height,
90 | class: "gg-legend-key"
91 | ),
92 | GGity.Shapes.draw(
93 | marker,
94 | {key_height / 2, key_height / 2 + key_height * index},
95 | :math.pow(1 + size, 2),
96 | fill: fixed_aesthetics[:fill],
97 | color: fixed_aesthetics[:color],
98 | fill_opacity: fixed_aesthetics[:alpha]
99 | ),
100 | Draw.text(
101 | "#{Labels.format(scale, level)}",
102 | x: "#{5 + key_height}",
103 | y: "#{10 + key_height * index}",
104 | class: "gg-text gg-legend-text",
105 | text_anchor: "left"
106 | )
107 | ]
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/lib/ggity/scale/size.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Size do
2 | @moduledoc false
3 |
4 | alias GGity.{Draw, Labels}
5 | alias GGity.Scale.{Continuous, Size}
6 |
7 | @base_intervals [0.1, 0.2, 0.25, 0.4, 0.5, 0.75, 1.0, 2.0, 2.5, 4.0, 5.0, 7.5, 10]
8 |
9 | defstruct range: {1, 6},
10 | breaks: 4,
11 | labels: :waivers,
12 | tick_values: [],
13 | inverse: nil,
14 | transform: nil,
15 | guide: :legend
16 |
17 | @type t() :: %__MODULE__{}
18 |
19 | @spec new(keyword()) :: Size.t()
20 | def new(options \\ []), do: struct(Size, options)
21 |
22 | @spec train(Size.t(), {number(), number()}) :: Size.t()
23 | def train(scale, {min, max}) when is_number(min) and is_number(max) do
24 | range = max - min
25 | struct(scale, transformations(range, min, max, scale))
26 | end
27 |
28 | defp transformations(range, min, max, %Size{} = scale) do
29 | raw_interval_size = range / (scale.breaks - 1)
30 | order_of_magnitude = :math.ceil(:math.log10(raw_interval_size) - 1)
31 | power_of_ten = :math.pow(10, order_of_magnitude)
32 | adjusted_interval_size = axis_interval_lookup(raw_interval_size / power_of_ten) * power_of_ten
33 | adjusted_min = adjusted_interval_size * Float.floor(min / adjusted_interval_size)
34 | adjusted_max = adjusted_interval_size * Float.ceil(max / adjusted_interval_size)
35 |
36 | adjusted_interval_count =
37 | round(1.0001 * (adjusted_max - adjusted_min) / adjusted_interval_size)
38 |
39 | # TODO - generalize/fix this so it is not different from other continuous scales
40 | tick_values =
41 | Enum.map(
42 | 1..(adjusted_interval_count + 1),
43 | &(adjusted_min + &1 * adjusted_interval_size)
44 | # &(adjusted_min + (&1 - 1) * adjusted_interval_size)
45 | )
46 |
47 | # This does not seem like it is exactly right depending on the shape
48 | # But it appears to be what ggplot2 does:
49 | # https://github.com/tidyverse/ggplot2/blob/master/R/scale-size.r
50 | # https://github.com/r-lib/scales/blob/master/R/pal-area.r
51 | domain_min = :math.pow(elem(scale.range, 0), 2)
52 | domain_max = :math.pow(elem(scale.range, 1), 2)
53 | transform = Continuous.transform({adjusted_min, adjusted_max}, {domain_min, domain_max})
54 |
55 | [
56 | tick_values: tick_values,
57 | inverse: transform,
58 | transform: transform
59 | ]
60 | end
61 |
62 | defp axis_interval_lookup(value) do
63 | Enum.find(@base_intervals, &(&1 >= value))
64 | end
65 |
66 | @spec draw_legend(Size.t(), binary(), atom(), number(), keyword()) :: iolist()
67 | def draw_legend(%Size{guide: :none}, _label, _key_glyph, _key_height, _fixed_aesthetics), do: []
68 |
69 | def draw_legend(%Size{} = scale, label, key_glyph, key_height, fixed_aesthetics) do
70 | max_value = Enum.max(scale.tick_values)
71 | key_width = max(key_height, 2 * :math.sqrt(scale.transform.(max_value) / :math.pi()))
72 |
73 | [
74 | Draw.text(
75 | "#{label}",
76 | x: "0",
77 | y: "-5",
78 | class: "gg-text gg-legend-title",
79 | text_anchor: "left"
80 | ),
81 | scale.tick_values
82 | |> Enum.map_reduce(0, fn value, y_position ->
83 | draw_legend_item(
84 | scale,
85 | value,
86 | key_glyph,
87 | key_height,
88 | key_width,
89 | y_position,
90 | fixed_aesthetics
91 | )
92 | end)
93 | |> elem(0)
94 | ]
95 | end
96 |
97 | defp draw_legend_item(
98 | scale,
99 | value,
100 | key_glyph,
101 | key_height,
102 | key_width,
103 | y_position,
104 | fixed_aesthetics
105 | ) do
106 | key_height = max(key_height, 2 * :math.sqrt(scale.transform.(value) / :math.pi()))
107 |
108 | {
109 | [
110 | Draw.rect(
111 | x: "0",
112 | y: "#{y_position}",
113 | height: key_height,
114 | width: key_width,
115 | class: "gg-legend-key"
116 | ),
117 | draw_key_glyph(
118 | scale,
119 | value,
120 | key_glyph,
121 | key_height,
122 | key_width,
123 | y_position,
124 | fixed_aesthetics
125 | ),
126 | Draw.text(
127 | "#{Labels.format(scale, value)}",
128 | x: "#{5 + key_width}",
129 | y: "#{key_height / 2 + y_position}",
130 | class: "gg-text gg-legend-text",
131 | text_anchor: "left",
132 | dominant_baseline: "middle"
133 | )
134 | ],
135 | y_position + key_height
136 | }
137 | end
138 |
139 | defp draw_key_glyph(scale, value, :a, key_height, key_width, y_position, fixed_aesthetics) do
140 | Draw.text(
141 | "a",
142 | x: "#{key_width / 2}",
143 | y: "#{key_height / 2 + y_position}",
144 | font_size: "#{scale.transform.(value)}pt",
145 | fill: fixed_aesthetics[:color],
146 | text_anchor: "left"
147 | )
148 | end
149 |
150 | defp draw_key_glyph(
151 | scale,
152 | value,
153 | _key_glyph,
154 | key_height,
155 | key_width,
156 | y_position,
157 | fixed_aesthetics
158 | ) do
159 | GGity.Shapes.draw(
160 | fixed_aesthetics[:shape],
161 | {key_width / 2, key_height / 2 + y_position},
162 | scale.inverse.(value),
163 | color: fixed_aesthetics[:color],
164 | fill_opacity: fixed_aesthetics[:alpha]
165 | )
166 | end
167 | end
168 |
169 | # The process here needs to be:
170 |
171 | # For each variable
172 | # 1) for each layer
173 | # 2) if that variable is mapped to one or more aesthetics with a legend as a guide
174 | # 3) get the breaks for the legend
175 |
176 | # You don't need to merge aesthetics across layers, you just draw glyphs on top of one another
177 | # What data do I need?
178 | # --A list of all the variables mapped to something with a legend
179 | # --Groups of layers, one for each variable
180 | # -- For each group, need the layers, the scale
181 |
182 | # For first layer - is there mapping to a variable with a legend as a guide?
183 | # If yes, note that and get the fixed aesthetics mapped for that layer
184 | # Go through the rest of the layers and do the same thing
185 | # With that list, for each break in the scale draw a glyph for each layer
186 |
187 | # So what have we learned
188 | # A legend displays specific values (breaks) and a glyph mapping that value to the applicable aesthetic
189 | # A colorbar is not a legend, it is a thing that draws a gradient over the range of the color aesthetic with tick
190 | # marks at the breaks
191 | # There is also a guide_bins, which displays the cutoffs for each bin on each side (top/bottom) of the glyph
192 | # for that bin
193 | # ggplot2 doesn't try to merge different kinds of guides
194 |
--------------------------------------------------------------------------------
/lib/ggity/scale/size_manual.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Size.Manual do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.Size
5 |
6 | @default_size 4
7 |
8 | @type t() :: %__MODULE__{}
9 |
10 | defstruct transform: nil
11 |
12 | @spec new(number()) :: Size.Manual.t()
13 | def new(size \\ @default_size) when is_number(size) do
14 | struct(Size.Manual, transform: fn _size -> size end)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/ggity/scale/x_continuous.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.X.Continuous do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.{Continuous, X}
5 |
6 | @base_axis_intervals [0.1, 0.2, 0.25, 0.4, 0.5, 0.75, 1.0, 2.0, 2.5, 4.0, 5.0, 7.5, 10]
7 |
8 | @type t() :: %__MODULE__{}
9 | @type mapping() :: map()
10 |
11 | defstruct width: 200,
12 | breaks: 5,
13 | labels: :waivers,
14 | tick_values: nil,
15 | inverse: nil,
16 | transform: nil
17 |
18 | @spec new(keyword()) :: X.Continuous.t()
19 | def new(options \\ []), do: struct(X.Continuous, options)
20 |
21 | @spec train(X.Continuous.t(), {number(), number()}) :: X.Continuous.t()
22 | def train(scale, {min, max}) do
23 | range = max - min
24 | struct(scale, transformations(range, min, max, scale))
25 | end
26 |
27 | defp transformations(0, min, _max, %X.Continuous{} = scale) do
28 | [
29 | tick_values: min,
30 | inverse: fn _value -> min end,
31 | transform: fn _value -> scale.width / 2 end
32 | ]
33 | end
34 |
35 | # Many parts of this library are influenced by ContEx, but this part (which is itself copied
36 | # in GGity.Scale.Y.Continuous) is more or less flat-out copied - license acknowledgement below:
37 |
38 | # Copyright (c) 2020 John Jessop (mindOk)
39 |
40 | # Permission is hereby granted, free of charge, to any person obtaining a copy
41 | # of this software and associated documentation files (the "Software"), to deal
42 | # in the Software without restriction, including without limitation the rights
43 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
44 | # copies of the Software, and to permit persons to whom the Software is
45 | # furnished to do so, subject to the following conditions:
46 |
47 | # The above copyright notice and this permission notice shall be included in all
48 | # copies or substantial portions of the Software.
49 |
50 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
52 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
53 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
54 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
55 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
56 | # SOFTWARE.
57 |
58 | defp transformations(range, min, max, %X.Continuous{} = scale) do
59 | raw_interval_size = range / (scale.breaks - 1)
60 | order_of_magnitude = :math.ceil(:math.log10(raw_interval_size) - 1)
61 | power_of_ten = :math.pow(10, order_of_magnitude)
62 | adjusted_interval_size = axis_interval_lookup(raw_interval_size / power_of_ten) * power_of_ten
63 | adjusted_min = adjusted_interval_size * Float.floor(min / adjusted_interval_size)
64 | adjusted_max = adjusted_interval_size * Float.ceil(max / adjusted_interval_size)
65 |
66 | adjusted_interval_count =
67 | round(1.0001 * (adjusted_max - adjusted_min) / adjusted_interval_size)
68 |
69 | tick_values =
70 | Enum.map(
71 | 1..(adjusted_interval_count + 1),
72 | &(adjusted_min + (&1 - 1) * adjusted_interval_size)
73 | )
74 |
75 | [
76 | tick_values: tick_values,
77 | inverse: Continuous.transform({adjusted_min, adjusted_max}, {0, scale.width}),
78 | transform: Continuous.transform({adjusted_min, adjusted_max}, {0, scale.width})
79 | ]
80 | end
81 |
82 | defp axis_interval_lookup(value) do
83 | Enum.find(@base_axis_intervals, &(&1 >= value))
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/ggity/scale/x_discrete.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.X.Discrete do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.X
5 |
6 | @type t() :: %__MODULE__{}
7 | @type record() :: map()
8 | @type mapping() :: map()
9 |
10 | defstruct width: 200,
11 | levels: nil,
12 | labels: :waivers,
13 | padding: 5,
14 | tick_values: nil,
15 | inverse: nil,
16 | transform: nil
17 |
18 | @spec new(keyword()) :: X.Discrete.t()
19 | def new(options \\ []), do: struct(X.Discrete, options)
20 |
21 | @spec train(X.Discrete.t(), list(binary())) :: X.Discrete.t()
22 | def train(scale, [level | _other_levels] = levels) when is_list(levels) and is_binary(level) do
23 | scale = struct(scale, levels: levels)
24 | struct(scale, transformations(scale))
25 | end
26 |
27 | defp transformations(scale) do
28 | number_of_levels = length(scale.levels)
29 |
30 | values_map =
31 | scale.levels
32 | |> Stream.with_index()
33 | |> Stream.map(fn {level, index} ->
34 | group_width = (scale.width - number_of_levels * (scale.padding - 1)) / number_of_levels
35 | {level, group_width * (index + 0.5) + index * scale.padding}
36 | end)
37 | |> Enum.into(%{})
38 |
39 | transform = fn value -> values_map[to_string(value)] end
40 |
41 | [
42 | tick_values: scale.levels,
43 | inverse: transform,
44 | transform: transform
45 | ]
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/ggity/scale/y_continuous.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Scale.Y.Continuous do
2 | @moduledoc false
3 |
4 | alias GGity.Scale.{Continuous, Y}
5 |
6 | @base_axis_intervals [0.1, 0.2, 0.25, 0.4, 0.5, 0.75, 1.0, 2.0, 2.5, 4.0, 5.0, 7.5, 10]
7 |
8 | @type t() :: %__MODULE__{}
9 | @type mapping() :: map()
10 |
11 | defstruct width: 200,
12 | breaks: 5,
13 | labels: :waivers,
14 | tick_values: nil,
15 | inverse: nil,
16 | transform: nil
17 |
18 | @spec new(keyword()) :: Y.Continuous.t()
19 | def new(options \\ []), do: struct(Y.Continuous, options)
20 |
21 | @spec train(Y.Continuous.t(), {number(), number()}) :: Y.Continuous.t()
22 | def train(scale, {min, max}) do
23 | range = max - min
24 | struct(scale, transformations(range, min, max, scale))
25 | end
26 |
27 | defp transformations(0, min, _max, %Y.Continuous{} = scale) do
28 | [
29 | tick_values: [min],
30 | inverse: fn _value -> scale.width / 2 end,
31 | transform: fn _value -> scale.width / 2 end
32 | ]
33 | end
34 |
35 | defp transformations(range, min, max, %Y.Continuous{} = scale) do
36 | raw_interval_size = range / (scale.breaks - 1)
37 | order_of_magnitude = :math.ceil(:math.log10(raw_interval_size) - 1)
38 | power_of_ten = :math.pow(10, order_of_magnitude)
39 | adjusted_interval_size = axis_interval_lookup(raw_interval_size / power_of_ten) * power_of_ten
40 | adjusted_min = adjusted_interval_size * Float.floor(min / adjusted_interval_size)
41 | adjusted_max = adjusted_interval_size * Float.ceil(max / adjusted_interval_size)
42 |
43 | adjusted_interval_count =
44 | round(1.0001 * (adjusted_max - adjusted_min) / adjusted_interval_size)
45 |
46 | tick_values =
47 | Enum.map(
48 | 1..(adjusted_interval_count + 1),
49 | &(adjusted_min + (&1 - 1) * adjusted_interval_size)
50 | )
51 |
52 | [
53 | tick_values: tick_values,
54 | inverse: Continuous.transform({adjusted_min, adjusted_max}, {0, scale.width}),
55 | transform: Continuous.transform({adjusted_min, adjusted_max}, {0, scale.width})
56 | ]
57 | end
58 |
59 | defp axis_interval_lookup(value) do
60 | Enum.find(@base_axis_intervals, &(&1 >= value))
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/ggity/stat.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Stat do
2 | @moduledoc false
3 |
4 | @type dataset :: Explorer.DataFrame.t()
5 |
6 | @doc false
7 | @spec identity(dataset(), map()) :: {dataset(), map()}
8 | def identity(data, mapping), do: {data, mapping}
9 |
10 | @spec count(dataset(), map()) :: {dataset(), map()}
11 | def count(data, mapping) do
12 | discrete_variables = discrete_variables(data, mapping)
13 |
14 | stat =
15 | data
16 | |> Explorer.DataFrame.group_by(discrete_variables)
17 | |> Explorer.DataFrame.summarise_with(&[count: Explorer.Series.count(&1[mapping[:x]])])
18 | |> Explorer.DataFrame.arrange_with(& &1[mapping[:x]])
19 |
20 | mapping = Map.put(mapping, :y, "count")
21 | {stat, mapping}
22 | end
23 |
24 | @spec boxplot(dataset(), map()) :: {dataset(), map()}
25 | def boxplot(data, mapping) do
26 | discrete_aesthetics = discrete_aesthetics(data, mapping)
27 | permutations = permutations(discrete_aesthetics, data, mapping)
28 | data = Explorer.DataFrame.to_rows(data)
29 |
30 | stat =
31 | permutations
32 | |> Enum.reduce([], fn permutation, stat ->
33 | [
34 | discrete_aesthetics
35 | |> Map.new(fn aesthetic ->
36 | {mapping[aesthetic], permutation[aesthetic]}
37 | end)
38 | |> Map.merge(boxplot_stats_map(data, mapping, permutation))
39 | | stat
40 | ]
41 | end)
42 | |> Enum.sort_by(fn row -> row[mapping[:x]] end)
43 | |> Explorer.DataFrame.new(dtypes: [{"outliers", :binary}])
44 |
45 | {stat, mapping}
46 | end
47 |
48 | defp boxplot_stats_map(data, mapping, permutation) do
49 | permutation_data =
50 | data
51 | |> Enum.filter(fn row ->
52 | Enum.map(permutation, fn {k, _v} -> row[mapping[k]] end) ==
53 | Enum.map(permutation, fn {_k, v} -> v end)
54 | end)
55 | |> Enum.map(fn row -> row[mapping[:y]] end)
56 |
57 | permutation_series = Explorer.Series.from_list(permutation_data)
58 |
59 | quantiles =
60 | for quantile <- [0.25, 0.5, 0.75],
61 | do: {quantile, Explorer.Series.quantile(permutation_series, quantile)},
62 | into: %{}
63 |
64 | interquartile_range = quantiles[0.75] - quantiles[0.25]
65 | ymin_threshold = quantiles[0.25] - 1.5 * interquartile_range
66 | ymax_threshold = quantiles[0.75] + 1.5 * interquartile_range
67 |
68 | outliers =
69 | for record <- permutation_data,
70 | record > ymax_threshold or record < ymin_threshold,
71 | do: record
72 |
73 | %{
74 | "ymin" =>
75 | permutation_series
76 | |> Explorer.Series.mask(Explorer.Series.greater_equal(permutation_series, ymin_threshold))
77 | |> Explorer.Series.min(),
78 | "lower" => quantiles[0.25],
79 | "middle" => quantiles[0.5],
80 | "upper" => quantiles[0.75],
81 | "ymax" =>
82 | permutation_series
83 | |> Explorer.Series.mask(Explorer.Series.less_equal(permutation_series, ymax_threshold))
84 | |> Explorer.Series.max(),
85 | "outliers" => :erlang.term_to_binary(outliers)
86 | }
87 | end
88 |
89 | defp discrete_variables(data, mapping) do
90 | for {name, series} <- Explorer.DataFrame.to_series(data),
91 | Explorer.Series.dtype(series) == :string or name == mapping[:x],
92 | name in Map.values(mapping),
93 | do: name
94 | end
95 |
96 | defp discrete_aesthetics(data, mapping) do
97 | discrete_variables = discrete_variables(data, mapping)
98 | for {aesthetic, variable} <- mapping, variable in discrete_variables, do: aesthetic
99 | end
100 |
101 | defp permutations(discrete_aesthetics, data, mapping) do
102 | data = Explorer.DataFrame.to_rows(data)
103 |
104 | for row <- data,
105 | uniq: true,
106 | do:
107 | discrete_aesthetics
108 | |> Enum.map(fn aesthetic -> {aesthetic, row[mapping[aesthetic]]} end)
109 | |> Map.new()
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/annotate.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Annotate do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.mtcars()
10 | |> Plot.new(%{x: :wt, y: :mpg})
11 | |> Plot.geom_point()
12 | |> Plot.annotate(:text, x: 4, y: 25, label: "Some text")
13 | """,
14 | """
15 | Examples.mtcars()
16 | |> Plot.new(%{x: :wt, y: :mpg})
17 | |> Plot.geom_point()
18 | |> Plot.annotate(:rect, xmin: 3, xmax: 4.2, ymin: 12, ymax: 21, alpha: 0.2)
19 | """,
20 | """
21 | Examples.mtcars()
22 | |> Plot.new(%{x: :wt, y: :mpg})
23 | |> Plot.geom_point()
24 | |> Plot.annotate(:segment, x: 2.5, xend: 4, y: 15, yend: 25, color: "blue")
25 | """
26 | ]
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/geom_bar.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Geom.Bar do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.mpg()
10 | |> Plot.new(%{x: "class"})
11 | |> Plot.geom_bar()
12 | """,
13 | """
14 | Examples.mpg()
15 | |> Plot.new(%{x: "class"})
16 | |> Plot.geom_bar(%{fill: "drv"})
17 | """
18 | ]
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/geom_boxplot.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Geom.Boxplot do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.mpg()
10 | |> Plot.new(%{x: "class", y: "hwy"})
11 | |> Plot.geom_boxplot()
12 | """,
13 | """
14 | Examples.mpg()
15 | |> Plot.new(%{x: "class", y: "hwy"})
16 | |> Plot.geom_boxplot(fill: "white", color: "#3366FF")
17 | """,
18 | """
19 | Examples.mpg()
20 | |> Plot.new(%{x: "class", y: "hwy"})
21 | |> Plot.geom_boxplot(outlier_color: "red")
22 | """,
23 | """
24 | Examples.mpg()
25 | |> Plot.new(%{x: "class", y: "hwy"})
26 | |> Plot.geom_boxplot(%{color: "drv"})
27 | """
28 | ]
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/geom_line.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Geom.Line do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.economics()
10 | |> Plot.new(%{x: "date", y: "unemploy"})
11 | |> Plot.geom_line()
12 | """,
13 | """
14 | Examples.economics_long()
15 | |> Plot.new(%{x: "date", y: "value01", color: "variable"})
16 | |> Plot.geom_line()
17 | """,
18 | """
19 | Examples.economics()
20 | |> Plot.new(%{x: "date", y: "unemploy"})
21 | |> Plot.geom_line(color: "red")
22 | """
23 | ]
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/geom_point.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Geom.Point do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.mtcars()
10 | |> Plot.new(%{x: :wt, y: :mpg})
11 | |> Plot.geom_point()
12 | """,
13 | """
14 | # Add aesthetic mapping to color
15 | Examples.mtcars()
16 | |> Plot.new(%{x: :wt, y: :mpg})
17 | |> Plot.geom_point(%{color: :cyl})
18 | """,
19 | """
20 | # Add aesthetic mapping to shape
21 | Examples.mtcars()
22 | |> Plot.new(%{x: :wt, y: :mpg})
23 | |> Plot.geom_point(%{shape: :cyl})
24 | """,
25 | """
26 | # Add aesthetic mapping to size (for circles, a bubble chart)
27 | Examples.mtcars()
28 | |> Plot.new(%{x: :wt, y: :mpg})
29 | |> Plot.geom_point(%{size: :qsec})
30 | """,
31 | """
32 | # Set aesthetics to fixed value
33 | Examples.mtcars()
34 | |> Plot.new(%{x: :wt, y: :mpg})
35 | |> Plot.geom_point(color: "red", size: 5)
36 | """
37 | ]
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/geom_text.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Geom.Text do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.mtcars()
10 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
11 | |> Plot.geom_text()
12 | """,
13 | """
14 | # Set the font size for the label
15 | Examples.mtcars()
16 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
17 | |> Plot.geom_text(size: 10)
18 | """,
19 | """
20 | # Shift positioning
21 | Examples.mtcars()
22 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
23 | |> Plot.geom_point(size: 2)
24 | |> Plot.geom_text(size: 5, hjust: :left, nudge_x: 3)
25 | """,
26 | """
27 | Examples.mtcars()
28 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
29 | |> Plot.geom_point(size: 2)
30 | |> Plot.geom_text(size: 5, vjust: :top, nudge_y: 3)
31 | """,
32 | """
33 | # Map other aesthetics
34 | Examples.mtcars()
35 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
36 | |> Plot.geom_text(%{color: :cyl}, size: 5)
37 | """,
38 | """
39 | # Add a text annotation
40 | Examples.mtcars()
41 | |> Plot.new(%{x: :wt, y: :mpg, label: :model})
42 | |> Plot.geom_text(size: 5)
43 | |> Plot.annotate(:text, label: "plot mpg vs. wt", x: 1.5, y: 15, size: 8, color: "red")
44 | """,
45 | """
46 | # Bar chart labelling
47 | [%{x: "1", y: 1, grp: "a"},
48 | %{x: "1", y: 3, grp: "b"},
49 | %{x: "2", y: 2, grp: "a"},
50 | %{x: "2", y: 1, grp: "b"},]
51 | |> Plot.new(%{x: "x", y: "y", group: "grp"})
52 | |> Plot.geom_col(%{fill: "grp"}, position: :dodge)
53 | |> Plot.geom_text(%{label: "y"}, position: :dodge, size: 6)
54 | """,
55 | """
56 | # Nudge the label up a bit
57 | [%{x: "1", y: 1, grp: "a"},
58 | %{x: "1", y: 3, grp: "b"},
59 | %{x: "2", y: 2, grp: "a"},
60 | %{x: "2", y: 1, grp: "b"},]
61 | |> Plot.new(%{x: "x", y: "y", group: "grp"})
62 | |> Plot.geom_col(%{fill: "grp"}, position: :dodge)
63 | |> Plot.geom_text(%{label: "y"}, position: :dodge, size: 6, nudge_y: 4)
64 | """,
65 | """
66 | # Position label in the middle of stacked bars
67 | [%{x: "1", y: 1, grp: "a"},
68 | %{x: "1", y: 3, grp: "b"},
69 | %{x: "2", y: 2, grp: "a"},
70 | %{x: "2", y: 1, grp: "b"},]
71 | |> Plot.new(%{x: "x", y: "y", group: "grp"})
72 | |> Plot.geom_col(%{fill: "grp"})
73 | |> Plot.geom_text(%{label: "y"}, position: :stack, position_vjust: 0.5, size: 6)
74 | """
75 | ]
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/scale_color_viridis.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Scale.Color.Viridis do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | # The viridis scale is the default color scale
10 | Examples.diamonds()
11 | |> Explorer.DataFrame.sample(1000, seed: 100)
12 | |> Plot.new(%{x: "carat", y: "price"})
13 | |> Plot.geom_point(%{color: "clarity"})
14 | """,
15 | """
16 | # Use the :option option to select a palette
17 | cities = Explorer.Series.from_list([
18 | "Houston",
19 | "Fort Worth",
20 | "San Antonio",
21 | "Dallas",
22 | "Austin"
23 | ])
24 |
25 | Examples.tx_housing()
26 | |> Explorer.DataFrame.filter_with(&Explorer.Series.in(&1["city"], cities))
27 | |> Plot.new(%{x: "sales", y: "median"})
28 | |> Plot.geom_point(%{color: "city"})
29 | |> Plot.scale_color_viridis(option: :plasma)
30 | """,
31 | """
32 | cities = Explorer.Series.from_list([
33 | "Houston",
34 | "Fort Worth",
35 | "San Antonio",
36 | "Dallas",
37 | "Austin"
38 | ])
39 |
40 | Examples.tx_housing()
41 | |> Explorer.DataFrame.filter_with(&Explorer.Series.in(&1["city"], cities))
42 | |> Plot.new(%{x: "sales", y: "median"})
43 | |> Plot.geom_point(%{color: "city"})
44 | |> Plot.scale_color_viridis(option: :inferno)
45 | """
46 | ]
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/mix/tasks/doc_examples/theme.ex:
--------------------------------------------------------------------------------
1 | defmodule GGity.Docs.Theme do
2 | @moduledoc false
3 |
4 | @doc false
5 | @spec examples() :: iolist()
6 | def examples do
7 | [
8 | """
9 | Examples.mtcars()
10 | |> Plot.new(%{x: :wt, y: :mpg})
11 | |> Plot.geom_point()
12 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
13 | """,
14 | """
15 | # Examples below assume that element constructors are imported
16 | # e.g. `import GGity.Element.{Line, Rect, Text}
17 |
18 | # Plot formatting
19 | Examples.mtcars()
20 | |> Plot.new(%{x: :wt, y: :mpg})
21 | |> Plot.geom_point()
22 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
23 | |> Plot.theme(plot_title: element_text(size: 10))
24 | """,
25 | """
26 | Examples.mtcars()
27 | |> Plot.new(%{x: :wt, y: :mpg})
28 | |> Plot.geom_point()
29 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
30 | |> Plot.theme(plot_background: element_rect(fill: "green"))
31 | """,
32 | """
33 | # Panel formatting
34 | Examples.mtcars()
35 | |> Plot.new(%{x: :wt, y: :mpg})
36 | |> Plot.geom_point()
37 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
38 | |> Plot.theme(panel_background: element_rect(fill: "white", color: "grey"))
39 | """,
40 | # TODO: Major gridlines should be drawn on top of minor gridlines.
41 | # Unfortunately we draw each axis and gridlines set as one SVG group,
42 | # so this will required material changes to the axis-drawing internals.
43 | """
44 | Examples.mtcars()
45 | |> Plot.new(%{x: :wt, y: :mpg})
46 | |> Plot.geom_point()
47 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
48 | |> Plot.theme(panel_grid_major: element_line(color: "black"))
49 | """,
50 | """
51 | # Axis formatting
52 | Examples.mtcars()
53 | |> Plot.new(%{x: :wt, y: :mpg})
54 | |> Plot.geom_point()
55 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
56 | |> Plot.theme(axis_line: element_line(size: 6, color: "grey"))
57 | """,
58 | """
59 | Examples.mtcars()
60 | |> Plot.new(%{x: :wt, y: :mpg})
61 | |> Plot.geom_point()
62 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
63 | |> Plot.theme(axis_text: element_text(color: "blue"))
64 | """,
65 | """
66 | Examples.mtcars()
67 | |> Plot.new(%{x: :wt, y: :mpg})
68 | |> Plot.geom_point()
69 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
70 | |> Plot.theme(axis_ticks: element_line(size: 4))
71 | """,
72 | """
73 | # Turn the x-axis ticks inward
74 | Examples.mtcars()
75 | |> Plot.new(%{x: :wt, y: :mpg})
76 | |> Plot.geom_point()
77 | |> Plot.labs(title: "Fuel economy declines as weight decreases")
78 | |> Plot.theme(axis_ticks_length_x: -2)
79 | """,
80 | """
81 | # GGity does not support legend position, but legend key boxes
82 | # and text can be styled as you would expect
83 |
84 | # Default styling
85 | Examples.mtcars()
86 | |> Plot.new(%{x: :wt, y: :mpg})
87 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
88 | |> Plot.labs(
89 | x: "Weight (1000 lbs)",
90 | y: "Fuel economy (mpg)",
91 | color: "Cylinders",
92 | shape: "Transmission"
93 | )
94 | """,
95 | """
96 | # Style legend keys
97 | Examples.mtcars()
98 | |> Plot.new(%{x: :wt, y: :mpg})
99 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
100 | |> Plot.labs(
101 | x: "Weight (1000 lbs)",
102 | y: "Fuel economy (mpg)",
103 | color: "Cylinders",
104 | shape: "Transmission"
105 | )
106 | |> Plot.theme(legend_key: element_rect(fill: "white", color: "black"))
107 | """,
108 | """
109 | # Style legend text
110 | Examples.mtcars()
111 | |> Plot.new(%{x: :wt, y: :mpg})
112 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
113 | |> Plot.labs(
114 | x: "Weight (1000 lbs)",
115 | y: "Fuel economy (mpg)",
116 | color: "Cylinders",
117 | shape: "Transmission"
118 | )
119 | |> Plot.theme(legend_text: element_text(size: 4, color: "red"))
120 | """,
121 | """
122 | # Style legend title
123 | Examples.mtcars()
124 | |> Plot.new(%{x: :wt, y: :mpg})
125 | |> Plot.geom_point(%{color: :cyl, shape: :vs})
126 | |> Plot.labs(
127 | x: "Weight (1000 lbs)",
128 | y: "Fuel economy (mpg)",
129 | color: "Cylinders",
130 | shape: "Transmission"
131 | )
132 | |> Plot.theme(legend_title: element_text(face: "bold"))
133 | """
134 | ]
135 | end
136 | end
137 |
--------------------------------------------------------------------------------
/lib/mix/tasks/docs.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Ggity.Docs do
2 | @shortdoc "Generates guides documentation."
3 | @moduledoc @shortdoc
4 |
5 | use Mix.Task
6 | alias GGity.Docs
7 |
8 | @examples [
9 | Docs.Geom.Point,
10 | Docs.Geom.Line,
11 | Docs.Geom.Bar,
12 | Docs.Geom.Boxplot,
13 | Docs.Geom.Text,
14 | Docs.Scale.Color.Viridis,
15 | Docs.Theme,
16 | Docs.Annotate
17 | ]
18 |
19 | @aliases "alias GGity.{Examples, Plot}\n"
20 |
21 | @element_import "import GGity.Element.{Line, Rect, Text}\n"
22 |
23 | @docs_code """
24 | |> Plot.plot()
25 | """
26 |
27 | @to_file_code """
28 | |> Plot.to_xml(550)
29 | """
30 |
31 | @doc false
32 | @spec run(list(any)) :: list(:ok)
33 | def run(_argv) do
34 | for example <- @examples, do: guides_for(example)
35 | end
36 |
37 | defp guides_for(example) do
38 | [_elixir, _ggity, _docs | example_module] =
39 | example
40 | |> Atom.to_string()
41 | |> String.downcase()
42 | |> String.split(".")
43 |
44 | name = Enum.join(example_module, "_")
45 |
46 | guide_content =
47 | example
48 | |> apply(:examples, [])
49 | |> Enum.with_index(1)
50 | |> Enum.map(fn example -> generate_example(name, example) end)
51 |
52 | File.write!("guides/#{name}.md", guide_content)
53 | end
54 |
55 | defp generate_example(name, {example, index}) do
56 | image_example(name, example, index)
57 | doc_example(name, example <> @docs_code, index)
58 | end
59 |
60 | defp image_example(name, example, index) do
61 | full_code = @aliases <> @element_import <> example <> @to_file_code
62 | {image, _mystery_list} = Code.eval_string(full_code)
63 | file = "guides/assets/#{name}_#{index}.svg"
64 | File.write!(file, image)
65 | end
66 |
67 | defp doc_example(name, example, index) do
68 | """
69 | ```
70 | #{example}
71 | ```
72 | 
73 | """
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/lib/mix/tasks/examples.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 | defmodule GGity.Examples do
3 | @moduledoc false
4 |
5 | def mtcars do
6 | headers = [:model, :mpg, :cyl, :disp, :hp, :drat, :wt, :qsec, :vs, :am, :gear, :carb]
7 |
8 | data = [
9 | ["Mazda RX4", 21, 6, 160, 110, 3.9, 2.62, 16.46, 0, 1, 4, 4],
10 | ["Mazda RX4 Wag", 21, 6, 160, 110, 3.9, 2.875, 17.02, 0, 1, 4, 4],
11 | ["Datsun 710", 22.8, 4, 108, 93, 3.85, 2.32, 18.61, 1, 1, 4, 1],
12 | ["Hornet 4 Drive", 21.4, 6, 258, 110, 3.08, 3.215, 19.44, 1, 0, 3, 1],
13 | ["Hornet Sportabout", 18.7, 8, 360, 175, 3.15, 3.44, 17.02, 0, 0, 3, 2],
14 | ["Valiant", 18.1, 6, 225, 105, 2.76, 3.46, 20.22, 1, 0, 3, 1],
15 | ["Duster 360", 14.3, 8, 360, 245, 3.21, 3.57, 15.84, 0, 0, 3, 4],
16 | ["Merc 240D", 24.4, 4, 146.7, 62, 3.69, 3.19, 20, 1, 0, 4, 2],
17 | ["Merc 230", 22.8, 4, 140.8, 95, 3.92, 3.15, 22.9, 1, 0, 4, 2],
18 | ["Merc 280", 19.2, 6, 167.6, 123, 3.92, 3.44, 18.3, 1, 0, 4, 4],
19 | ["Merc 280C", 17.8, 6, 167.6, 123, 3.92, 3.44, 18.9, 1, 0, 4, 4],
20 | ["Merc 450SE", 16.4, 8, 275.8, 180, 3.07, 4.07, 17.4, 0, 0, 3, 3],
21 | ["Merc 450SL", 17.3, 8, 275.8, 180, 3.07, 3.73, 17.6, 0, 0, 3, 3],
22 | ["Merc 450SLC", 15.2, 8, 275.8, 180, 3.07, 3.78, 18, 0, 0, 3, 3],
23 | ["Cadillac Fleetwood", 10.4, 8, 472, 205, 2.93, 5.25, 17.98, 0, 0, 3, 4],
24 | ["Lincoln Continental", 10.4, 8, 460, 215, 3, 5.424, 17.82, 0, 0, 3, 4],
25 | ["Chrysler Imperial", 14.7, 8, 440, 230, 3.23, 5.345, 17.42, 0, 0, 3, 4],
26 | ["Fiat 128", 32.4, 4, 78.7, 66, 4.08, 2.2, 19.47, 1, 1, 4, 1],
27 | ["Honda Civic", 30.4, 4, 75.7, 52, 4.93, 1.615, 18.52, 1, 1, 4, 2],
28 | ["Toyota Corolla", 33.9, 4, 71.1, 65, 4.22, 1.835, 19.9, 1, 1, 4, 1],
29 | ["Toyota Corona", 21.5, 4, 120.1, 97, 3.7, 2.465, 20.01, 1, 0, 3, 1],
30 | ["Dodge Challenger", 15.5, 8, 318, 150, 2.76, 3.52, 16.87, 0, 0, 3, 2],
31 | ["AMC Javelin", 15.2, 8, 304, 150, 3.15, 3.435, 17.3, 0, 0, 3, 2],
32 | ["Camaro Z28", 13.3, 8, 350, 245, 3.73, 3.84, 15.41, 0, 0, 3, 4],
33 | ["Pontiac Firebird", 19.2, 8, 400, 175, 3.08, 3.845, 17.05, 0, 0, 3, 2],
34 | ["Fiat X1-9", 27.3, 4, 79, 66, 4.08, 1.935, 18.9, 1, 1, 4, 1],
35 | ["Porsche 914-2", 26, 4, 120.3, 91, 4.43, 2.14, 16.7, 0, 1, 5, 2],
36 | ["Lotus Europa", 30.4, 4, 95.1, 113, 3.77, 1.513, 16.9, 1, 1, 5, 2],
37 | ["Ford Pantera L", 15.8, 8, 351, 264, 4.22, 3.17, 14.5, 0, 1, 5, 4],
38 | ["Ferrari Dino", 19.7, 6, 145, 175, 3.62, 2.77, 15.5, 0, 1, 5, 6],
39 | ["Maserati Bora", 15, 8, 301, 335, 3.54, 3.57, 14.6, 0, 1, 5, 8],
40 | ["Volvo 142E", 21.4, 4, 121, 109, 4.11, 2.78, 18.6, 1, 1, 4, 2]
41 | ]
42 |
43 | data
44 | |> Enum.map(fn row -> Enum.zip([headers, row]) end)
45 | |> Explorer.DataFrame.new()
46 | end
47 |
48 | def diamonds, do: load_csv("diamonds.csv")
49 | def economics, do: load_csv("economics.csv", parse_dates: true)
50 | def economics_long, do: load_csv("economics_long.csv", parse_dates: true)
51 | def mpg, do: load_csv("mpg.csv")
52 | def tx_housing, do: load_csv("tx_housing.csv")
53 |
54 | defp load_csv(file_name, options \\ []) do
55 | file_path = Path.join([:code.priv_dir(:ggity), file_name])
56 | Explorer.DataFrame.from_csv!(file_path, options)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule GGity.MixProject do
2 | use Mix.Project
3 |
4 | @source_url "https://github.com/srowley/ggity"
5 | @version "0.5.0"
6 |
7 | def project do
8 | [
9 | app: :ggity,
10 | version: @version,
11 | aliases: aliases(),
12 | elixir: "~> 1.14",
13 | start_permanent: Mix.env() == :prod,
14 | package: package(),
15 | deps: deps(),
16 | dialyzer: [plt_add_apps: [:mix]],
17 | name: "GGity",
18 | description: """
19 | GGity brings the familiar interface of R's ggplot2 library to SVG
20 | charting in Elixir.
21 | """,
22 | docs: docs()
23 | ]
24 | end
25 |
26 | def application, do: []
27 |
28 | defp deps do
29 | [
30 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
31 | {:ex_doc, "~> 0.29", only: :dev, runtime: false},
32 | {:sweet_xml, "~> 0.7", only: :test},
33 | {:explorer, "~> 0.6.0"},
34 | {:nimble_csv, "~> 1.2"},
35 | {:nimble_strftime, "~> 0.1"}
36 | ]
37 | end
38 |
39 | defp aliases do
40 | [
41 | checks: [
42 | "compile",
43 | "credo",
44 | "format"
45 | ],
46 | build_docs: [
47 | "ggity.docs",
48 | "docs"
49 | ]
50 | ]
51 | end
52 |
53 | defp package() do
54 | [
55 | maintainers: "Steve Rowley",
56 | name: "ggity",
57 | files: ~w(lib priv mix.exs README* LICENSE* ROADMAP* CHANGELOG*),
58 | licenses: ["MIT"],
59 | links: %{
60 | "Website" => "http://www.pocketbookvote.com/",
61 | "GitHub" => @source_url,
62 | "Changelog" => "#{@source_url}/blob/master/CHANGELOG.md",
63 | "Roadmap" => "#{@source_url}/blob/master/ROADMAP.md"
64 | }
65 | ]
66 | end
67 |
68 | defp docs do
69 | [
70 | source_url: @source_url,
71 | source_ref: "v#{@version}",
72 | main: "readme",
73 | extra_section: "CONCEPTS & EXAMPLES",
74 | assets: "guides/assets",
75 | formatters: ["html", "epub"],
76 | groups_for_modules: groups_for_modules(),
77 | extras: extras(),
78 | groups_for_extras: groups_for_extras(),
79 | api_reference: false
80 | ]
81 | end
82 |
83 | defp extras() do
84 | [
85 | "README.md",
86 | "guides/geom_point.md": [title: "Points"],
87 | "guides/geom_line.md": [title: "Lines"],
88 | "guides/geom_bar.md": [title: "Bars"],
89 | "guides/geom_boxplot.md": [title: "Boxplot"],
90 | "guides/geom_text.md": [title: "Text"],
91 | "guides/scale_color_viridis.md": [title: "Color/Fill Viridis"],
92 | "guides/theme.md": [title: "Theme"],
93 | "guides/annotate.md": [title: "Annotate"]
94 | ]
95 | end
96 |
97 | defp groups_for_extras do
98 | [
99 | Geoms: [
100 | "guides/geom_point.md",
101 | "guides/geom_line.md",
102 | "guides/geom_bar.md",
103 | "guides/geom_boxplot.md",
104 | "guides/geom_text.md"
105 | ],
106 | Scales: [
107 | "guides/scale_color_viridis.md"
108 | ],
109 | Themes: [
110 | "guides/theme.md"
111 | ],
112 | Annotations: [
113 | "guides/annotate.md"
114 | ]
115 | ]
116 | end
117 |
118 | defp groups_for_modules do
119 | [
120 | "Plot API": [GGity.Plot],
121 | Themes: [GGity.Theme, GGity.Element.Line, GGity.Element.Rect, GGity.Element.Text],
122 | Helpers: [GGity.Labels]
123 | ]
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3 | "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
4 | "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"},
5 | "earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"},
6 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
7 | "explorer": {:hex, :explorer, "0.6.1", "04b599781ed75e5eee076e3477a22a2a848eb1f17b02b122af7569e2ac37e719", [:mix], [{:nx, "~> 0.4.0 or ~> 0.5.0", [hex: :nx, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.5", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "43db8bf4c503e241e456e758fd8ff229be88495ac1c92ac820d7c4d215b2faff"},
8 | "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
10 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
11 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
13 | "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
14 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
15 | "nimble_strftime": {:hex, :nimble_strftime, "0.1.1", "b988184d1bd945bc139b2c27dd00a6c0774ec94f6b0b580083abd62d5d07818b", [:mix], [], "hexpm", "89e599c9b8b4d1203b7bb5c79eb51ef7c6a28fbc6228230b312f8b796310d755"},
16 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.6.2", "d2218ba08a43fa331957f30481d00b666664d7e3861431b02bd3f4f30eec8e5b", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "b9048eaed8d7d14a53f758c91865cc616608a438d2595f621f6a4b32a5511709"},
17 | "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
18 | "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
19 | "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
20 | }
21 |
--------------------------------------------------------------------------------
/test/ggity_draw_test.exs:
--------------------------------------------------------------------------------
1 | defmodule GGityDrawTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias GGity.Draw
5 |
6 | describe "svg/2" do
7 | test "wraps given IO list in