Skip to content

Commit 3945350

Browse files
authored
Refactor Enum to Streams - closes #3 (#6)
* Refactor datasets to Streams. Closes #3 * Refactor to Streams
1 parent 8dabbd1 commit 3945350

File tree

9 files changed

+79
-49
lines changed

9 files changed

+79
-49
lines changed

README.md

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Gnuplot Elixir
22

3-
A simple interface from [Elixir data][7] to the [Gnuplot graphing utility][1] that uses [Erlang Ports][5] to transmit data from your application to Gnuplot. Datasets are streamed directly to STDIN without temporary files and you can plot [1M points in 20 seconds](examples/stress.exs) on an AWS t2.medium 2 vCPU server.
3+
A simple interface from [Elixir data][7] to the [Gnuplot graphing utility][1] that uses [Erlang Ports][5] to transmit data from your application to Gnuplot. Datasets are streamed directly to STDIN without temporary files and you can plot [1M points in 12.7 seconds](examples/stress.exs).
44

55
Please visit the [Gnuplot demos gallery](http://gnuplot.sourceforge.net/demo/) to see all the possibilities, the [manual which describes the grammar](http://www.gnuplot.info/docs_5.2/Gnuplot_5.2.pdf), and the [examples folder](examples/).
66

@@ -11,7 +11,7 @@ This is a conversion of the [Clojure Gnuplot library][4] by [aphyr][2].
1111
The `plot` function takes two arguments:
1212

1313
* a list of commands (each of which is a list of terms)
14-
* a list of datasets (not required when plotting functions)
14+
* a list of Streams or Enumerable datasets (not required when plotting functions)
1515

1616
Commands are lists of terms that normally start with an atom such as `:set`. They may be written as lists or [Word lists](https://elixir-lang.org/getting-started/sigils.html#word-lists) - the following lines are equivalent:
1717

@@ -44,7 +44,7 @@ Gnuplot will by default open a window containing your plot:
4444

4545
### PNG of two datasets
4646

47-
Write two datasets to a file:
47+
Write two datasets to a PNG file:
4848

4949
```elixir
5050
G.plot([
@@ -56,7 +56,7 @@ G.plot([
5656
G.list(
5757
["-", :title, "uniform", :with, :points],
5858
["-", :title, "normal", :with, :points])]
59-
],
59+
],
6060
[
6161
for(n <- 0..200, do: [n, n * :rand.uniform()]),
6262
for(n <- 0..200, do: [n, n * :rand.normal()])
@@ -95,7 +95,7 @@ by adding `gnuplot` to your list of dependencies in `mix.exs`:
9595
```elixir
9696
def deps do
9797
[
98-
{:gnuplot, "~> 0.19.71"}
98+
{:gnuplot, "~> 0.19.72"}
9999
]
100100
end
101101
```
@@ -110,17 +110,19 @@ Some tests create plots which require `gnuplot` to be installed. They can be be
110110

111111
## Performance
112112

113-
The performance of the library on a MacBook Air is comparable to the Clojure version when `gnuplot` draws to a GUI. It is a little faster when writing directly to a PNG. It is faster still when running on a server. The times below are in milliseconds. Each plot was made in increasing order of the number of points and after a cold start of the VM.
113+
The performance of the library on a MacBook Air is comparable to the Clojure version when `gnuplot` draws to a GUI. It is a little faster when writing directly to a PNG when running on a server. The times below are in milliseconds. Each plot was made in increasing order of the number of points and after a cold start of the VM. The last two columns show the refactoring from Enumerable to Streams.
114114

115-
| Points | Clojure GUI | Elixir GUI | Elixir PNG | Ubuntu 16.04 |
116-
| -----: | ----------: | ---------: | ---------: | -----------: |
117-
| 1 | 1,487 | 5 | 2 | 4 |
118-
| 10 | 1,397 | 10 | 10 | <1 |
119-
| 1e2 | 1,400 | 4 | 49 | 1 |
120-
| 1e3 | 1,381 | 59 | 40 | 8 |
121-
| 1e4 | 1,440 | 939 | 349 | 211 |
122-
| 1e5 | 5,784 | 5,801 | 4,091 | 1,873 |
123-
| 1e6 | 49,275 | 43,464 | 41,521 | 19,916 |
115+
| Points | Clojure GUI | Elixir GUI | Elixir PNG | Elixir Enum | Elixir Stream |
116+
| -----: | ----------: | ---------: | ---------: | ------------: | ------------: |
117+
| 1 | 1,487 | 5 | 18 | 4 | 5 |
118+
| 10 | 1,397 | 10 | 1 | <1 | 1 |
119+
| 1e2 | 1,400 | 4 | 12 | 1 | 1 |
120+
| 1e3 | 1,381 | 59 | 52 | 8 | 10 |
121+
| 1e4 | 1,440 | 939 | 348 | 211 | 211 |
122+
| 1e5 | 5,784 | 5,801 | 3,494 | 1,873 | 1,313 |
123+
| 1e6 | 49,275 | 43,464 | 35,505 | 19,916 | 12,775 |
124+
| | MacBook | MacBook | MacBook | Ubuntu 16.04 | Ubuntu 16.04 |
125+
| | 2.5 GHz i5 | 2.5 GHz i5 | 2.5 GHz i5 | 3.3 GHz 2vCPU | 3.3 GHz 2vCPU |
124126

125127
![performance](docs/perf.PNG)
126128

@@ -130,7 +132,8 @@ clojure_gui = [1.487, 1.397, 1.400, 1.381, 1.440, 5.784, 49.275]
130132
elixir_gui = [0.005, 0.010, 0.004, 0.059, 0.939, 5.801, 43.464]
131133
elixir_png = [0.002, 0.010, 0.049, 0.040, 0.349, 4.091, 41.521]
132134
ubuntu_t2m = [0.004, 0.002, 0.001, 0.008, 0.211, 1.873, 19.916]
133-
datasets = for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m], do: Enum.zip(points, ds)
135+
ubuntu_stream = [0.002, 0.001, 0.001, 0.009, 0.204, 1.279, 12.858]
136+
for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m, ubuntu_stream], do: Enum.zip (points, ds)
134137

135138
G.plot([
136139
[:set, :title, "Render scatter plot"],
@@ -140,14 +143,16 @@ G.plot([
140143
~w(set logscale xy)a,
141144
~w(set grid xtics ytics)a,
142145
~w(set style line 1 lw 4 lc '#63b132')a,
143-
~w(set style line 2 lw 4 lc '#421C52')a,
144-
~w(set style line 3 lw 4 lc '#732C7B')a,
146+
~w(set style line 2 lw 4 lc '#2C001E')a,
147+
~w(set style line 3 lw 4 lc '#5E2750')a,
145148
~w(set style line 4 lw 4 lc '#E95420')a,
149+
~w(set style line 5 lw 4 lc '#77216F')a,
146150
[:plot, G.list(
147-
["-", :title, "Clojure GUI", :with, :lines, :ls, 1],
148-
["-", :title, "Elixir GUI", :with, :lines, :ls, 2],
149-
["-", :title, "Elixir PNG", :with, :lines, :ls, 3],
150-
["-", :title, "Elixir PNG t2.m", :with, :lines, :ls, 4]
151+
["-", :title, "Clojure GUI", :with, :lines, :ls, 1],
152+
["-", :title, "Elixir GUI", :with, :lines, :ls, 2],
153+
["-", :title, "Elixir PNG", :with, :lines, :ls, 3],
154+
["-", :title, "Elixir t2.m", :with, :lines, :ls, 4],
155+
["-", :title, "Elixir Stream", :with, :lines, :ls, 5]
151156
)]], datasets
152157
])
153158
```

docs/perf.PNG

243 Bytes
Loading

examples/perf.exs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,30 @@ defmodule Perf do
2020
~w(set logscale xy)a,
2121
~w(set grid xtics ytics)a,
2222
~w(set style line 1 lw 4 lc '#63b132')a,
23-
~w(set style line 2 lw 4 lc '#421C52')a,
24-
~w(set style line 3 lw 4 lc '#732C7B')a,
23+
~w(set style line 2 lw 4 lc '#2C001E')a,
24+
~w(set style line 3 lw 4 lc '#5E2750')a,
2525
~w(set style line 4 lw 4 lc '#E95420')a,
26+
~w(set style line 5 lw 4 lc '#77216F')a,
2627
[
2728
:plot,
2829
G.list(
29-
["-", :title, "Clojure GUI MB", :with, :lines, :ls, 1],
30-
["-", :title, "Elixir GUI MB", :with, :lines, :ls, 2],
31-
["-", :title, "Elixir PNG MB", :with, :lines, :ls, 3],
32-
["-", :title, "Elixir PNG t2.m", :with, :lines, :ls, 4]
30+
["-", :title, "Clojure GUI", :with, :lines, :ls, 1],
31+
["-", :title, "Elixir GUI", :with, :lines, :ls, 2],
32+
["-", :title, "Elixir PNG", :with, :lines, :ls, 3],
33+
["-", :title, "Elixir t2.m", :with, :lines, :ls, 4],
34+
["-", :title, "Elixir Stream", :with, :lines, :ls, 5]
3335
)
3436
]
3537
]
3638

3739
def data do
3840
points = [1, 10, 100, 1000, 10_000, 100_000, 1_000_000]
39-
clojure_gui = [1.487, 1.397, 1.400, 1.381, 1.440, 5.784, 49.275]
40-
elixir_gui = [0.005, 0.010, 0.004, 0.059, 0.939, 5.801, 43.464]
41-
elixir_png = [0.002, 0.010, 0.049, 0.040, 0.349, 4.091, 41.521]
42-
ubuntu_t2m = [0.004, 0.002, 0.001, 0.008, 0.211, 1.873, 19.916]
43-
for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m], do: Enum.zip(points, ds)
41+
clojure_gui = [1.487, 1.397, 1.400, 1.381, 1.440, 5.784, 49.275]
42+
elixir_gui = [0.005, 0.010, 0.004, 0.059, 0.939, 5.801, 43.464]
43+
elixir_png = [0.018, 0.001, 0.012, 0.052, 0.348, 3.494, 35.505]
44+
ubuntu_t2m = [0.004, 0.002, 0.001, 0.008, 0.211, 1.873, 19.916]
45+
ubuntu_stream = [0.002, 0.001, 0.001, 0.009, 0.204, 1.279, 12.858]
46+
for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m, ubuntu_stream], do: Enum.zip(points, ds)
4447
end
4548

4649
def plot, do: G.plot(target() ++ commands(), data())

examples/sine.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ defmodule Sine do
1919
'sin(x)',
2020
:title,
2121
"Sine Wave",
22-
:ls, 1
22+
:ls,
23+
1
2324
]
2425
]
2526

examples/stress.exs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ defmodule Stress do
1919
]
2020

2121
def data(n),
22-
do: [
23-
for(i <- 0..n, do: [i / n, i * :rand.uniform()])
24-
]
25-
26-
def plot(n), do: G.plot(target(n) ++ commands(), data(n))
22+
do:
23+
Stream.unfold(0, fn i ->
24+
if i <= n do
25+
{[i / n, i * :rand.uniform()], i + 1}
26+
else
27+
nil
28+
end
29+
end)
30+
31+
def plot(n), do: G.plot(target(n) ++ commands(), [data(n)])
2732
end
2833

2934
# time mix run examples/stress.exs

lib/gnuplot.ex

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,22 @@ defmodule Gnuplot do
2828
def plot(commands, datasets) do
2929
with {:ok, path} = gnuplot_bin(),
3030
cmd = Commands.format(commands),
31-
data = format_datasets(datasets),
3231
args = ["-p", "-e", cmd],
3332
port = Port.open({:spawn_executable, path}, [:binary, args: args]) do
34-
Enum.each(data, fn row -> send(port, {self(), {:command, row}}) end)
33+
transmit(port, datasets)
3534
{_, :close} = send(port, {self(), :close})
3635
{:ok, cmd}
3736
end
3837
end
3938

39+
defp transmit(port, datasets) do
40+
:ok =
41+
datasets
42+
|> format_datasets()
43+
|> Stream.each(fn row -> send(port, {self(), {:command, row}}) end)
44+
|> Stream.run()
45+
end
46+
4047
@doc """
4148
Plot a function that has no dataset.
4249
"""
@@ -54,6 +61,8 @@ defmodule Gnuplot do
5461

5562
def list(a, b, c, d), do: %Commands.List{xs: [a, b, c, d]}
5663

64+
def list(a, b, c, d, e), do: %Commands.List{xs: [a, b, c, d, e]}
65+
5766
@doc """
5867
Find the gnuplot executable.
5968
"""

lib/gnuplot/dataset.ex

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,19 @@ defmodule Gnuplot.Dataset do
1212
@gnuplot_end_data "\ne\n"
1313

1414
@doc """
15-
Convert a list of datasets.
15+
Format datasets into Gnuplot STDIN format. Datasets must be Enumerable and can be Streams.
1616
1717
A dataset is a list of points, each point is a list of numbers.
1818
"""
19-
@spec format_datasets(list(t())) :: [String.t()]
2019
def format_datasets(datasets) do
21-
Enum.flat_map(datasets, &format_dataset/1)
20+
Stream.flat_map(datasets, &format_dataset/1)
2221
end
2322

24-
@spec format_dataset(t()) :: [String.t()]
2523
defp format_dataset(dataset) do
2624
dataset
27-
|> Enum.map(&format_point/1)
28-
|> Enum.intersperse(@gnuplot_end_row)
29-
|> Enum.concat([@gnuplot_end_data])
25+
|> Stream.map(&format_point/1)
26+
|> Stream.intersperse(@gnuplot_end_row)
27+
|> Stream.concat([@gnuplot_end_data])
3028
end
3129

3230
@spec format_point(point()) :: String.t()

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule Gnuplot.MixProject do
44
def project do
55
[
66
app: :gnuplot,
7-
version: "0.19.71",
7+
version: "0.19.73",
88
elixir: "~> 1.4",
99
start_permanent: Mix.env() == :prod,
1010
description: "Interface between Elixir and Gnuplot graphing library",

test/gnuplot_test.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule GnuplotTest do
22
use ExUnit.Case
33
alias Gnuplot, as: G
44
alias Gnuplot.Commands, as: C
5+
alias Gnuplot.Dataset, as: D
56

67
test "List of commands" do
78
assert "set xtics off;\nset ytics off" ==
@@ -33,6 +34,14 @@ defmodule GnuplotTest do
3334
assert "set title \"simple's\"" == C.format([[:set, :title, "simple's"]])
3435
end
3536

37+
test "Datasets" do
38+
input = [[[1, 1], [1, 2]], [[2, 3], [2, 4], [2, 5]]]
39+
expected = ["1 1", "\n", "1 2", "\ne\n", "2 3", "\n", "2 4", "\n", "2 5", "\ne\n"]
40+
41+
assert expected ==
42+
input |> D.format_datasets() |> Enum.to_list()
43+
end
44+
3645
@tag gnuplot: true
3746
test "Gnuplot is installed" do
3847
assert {:ok, path} = G.gnuplot_bin()

0 commit comments

Comments
 (0)