├── .gitignore ├── Makefile ├── README.md ├── bin └── eplot_test ├── example ├── data1.dat ├── data2.dat └── test1.png ├── priv └── eplot │ └── priv │ └── fonts │ └── 6x11_latin1.wingsfont ├── rebar.config ├── src ├── egd_chart.erl ├── egd_colorscheme.erl ├── eplot.app.src ├── eplot.erl ├── eplot_main.erl └── eplot_view.erl └── test ├── egd_bar2d_SUITE.erl ├── egd_chart_SUITE.erl └── egd_colorscheme_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | ebin/ 3 | doc/ 4 | rebar3 5 | rebar.lock 6 | *.swp 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR3_URL=https://s3.amazonaws.com/rebar3/rebar3 2 | 3 | ifeq ($(wildcard rebar3),rebar3) 4 | REBAR3 = $(CURDIR)/rebar3 5 | endif 6 | 7 | REBAR3 ?= $(shell test -e `which rebar3` 2>/dev/null && which rebar3 || echo "./rebar3") 8 | 9 | ifeq ($(REBAR3),) 10 | REBAR3 = $(CURDIR)/rebar3 11 | endif 12 | 13 | .PHONY: deps escpritize test build 14 | 15 | all: build escriptize docs 16 | 17 | build: $(REBAR3) 18 | @$(REBAR3) compile 19 | 20 | $(REBAR3): 21 | wget $(REBAR3_URL) || curl -Lo rebar3 $(REBAR3_URL) 22 | chmod a+x rebar3 23 | 24 | escriptize: 25 | @$(REBAR3) escriptize 26 | 27 | deps: 28 | @$(REBAR3) get-deps 29 | 30 | clean: 31 | @$(REBAR3) clean 32 | 33 | distclean: clean 34 | @$(REBAR3) delete-deps 35 | 36 | docs: 37 | @$(REBAR3) edoc 38 | 39 | 40 | test: 41 | @$(REBAR3) do ct, cover 42 | 43 | 44 | release: test 45 | @$(REBAR3) release 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Eplot 2 | ===== 3 | 4 | Eplot is a small graph drawing tool that can be used from command prompt via the 5 | escript eplot or it could be used from erlang code using egd_graph module. 6 | 7 | Dependencies: 8 | ------------- 9 | - Erlang/OTP, preferable R13B but earlier should work. 10 | 11 | - wx, if you plan to view graph immediately Erlang should be compiled 12 | with wx support enabled. Otherwise only file output can be used. 13 | 14 | 15 | Eplot usage: 16 | ------------ 17 | eplot [options] file1 file2 ... 18 | 19 | where options are: 20 | 21 | -o Filename, defaults to png image-format, 22 | -type Type, bitmap_raw | png | eps, 23 | -width W, Width, in pixels, of output, 24 | -height H, Height, in pixels, of output, 25 | -render_engine RE, alpha | opaque, type of render engine, 26 | -plot Plot, plot2d | bar2d, plot type 27 | -x_label Label, X-axis label, 28 | -y_label Label, Y-axis label, 29 | -x_ticksize TS, X-axis ticksize, 30 | -y_ticksize TS, y-axis ticksize, 31 | 32 | example: 33 | 34 | $> bin/eplot -o test1.png example/data1.dat example/data2.dat 35 | 36 | 37 | egd_chart.erl usage: 38 | -------------------- 39 | See [source file](https://github.com/psyeugenic/eplot/blob/master/src/egd_chart.erl) for info. 40 | 41 | 42 | 43 | Eplot ToDo: 44 | ----------- 45 | - document stuff 46 | - different symbols for different line entries 47 | - support multiple font and sizes (egd dependent) 48 | - line thickness (egd dependent) 49 | - additional graph types 50 | 51 | EGD ToDo: 52 | --------- 53 | eplot uses EGD as an backend to draw graphs. EGD lacks some features which 54 | should be implemented. 55 | 56 | - polygon triangulation, filled triangles can be drawn fine but not 57 | polygons. 58 | - Truetype support 59 | - Line thickness/stroke size 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /bin/eplot_test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -smp enable 3 | %% vim: filetype=erlang 4 | 5 | main(["rendertest"]) -> rendertest(); 6 | main(["examples"]) -> examples(); 7 | main(_) -> halt(1). 8 | 9 | 10 | rendertest() -> 11 | code:add_patha(ebin()), 12 | Functions = [graph, bar2d], 13 | Options = [[ 14 | {width, Width}, {height, Height}, {margin, Margin}, 15 | {ticksize, Ticksize}, {x_range, Xrange}, {y_range, Yrange}, 16 | {y_label, Ylabel}, {x_label, Xlabel}, {bg_rgba, Rgba}] || 17 | 18 | Width <- [800, 1300], 19 | Height <- [800, 1024], 20 | Margin <- [30, 60], 21 | Ticksize <- [{12,12}, {180,180}], 22 | Xrange <- [{0, 100}, {-1000, 1000}], 23 | Yrange <- [{0, 100}, {-1000, 1000}], 24 | Ylabel <- ["Y-label"], 25 | Xlabel <- ["X-label"], 26 | Rgba <- [{255,255,255}] 27 | ], 28 | 29 | Ns = [100, 200, 500], 30 | 31 | Datas = [{"dataset " ++ integer_to_list(N), [{X,random:uniform(N)} || X <-lists:seq(1,N,10)]} || N <- Ns], 32 | 33 | Tests = [{Function, Option} || Function <- Functions, Option <- Options], 34 | 35 | lists:foldl(fun 36 | ({Function, Opts}, I) -> 37 | io:format("[~3w] ~p with ~p~n", [I, Function, Opts]), 38 | egd_chart:Function(Datas, Opts), 39 | I + 1 40 | end, 1, Tests), 41 | ok. 42 | 43 | 44 | examples() -> 45 | code:add_patha(ebin()), 46 | Functions = [graph, bar2d], 47 | Options = [[ 48 | {width, Width}, {height, Height}, {margin, Margin}, {render_engine, opaque}, 49 | {ticksize, Ticksize}, {x_range, {-300, 300}}, Yranges, 50 | {y_label, Ylabel}, {x_label, Xlabel}, {bg_rgba, Rgba}] || 51 | 52 | Width <- [1000], 53 | Height <- [800], 54 | Margin <- [45], 55 | Ticksize <- [{100,50}], 56 | Yranges <- [{y_range, {0,500}}, {y_range, {-500,500}}], 57 | Ylabel <- ["Y-label"], 58 | Xlabel <- ["X-label"], 59 | Rgba <- [{242,242,249,255}] 60 | ], 61 | 62 | Ns = [ 63 | {{0,200}, fun(X) -> 64 | 500/(X + 1) + X 65 | end}, 66 | {{-200,200}, fun(X) -> 67 | math:sqrt(X*X + 10*X) 68 | end}, 69 | {{-300,300}, fun(X) -> 70 | X*math:sin(X/200*6.28) 71 | end} 72 | ], 73 | 74 | Datas = [ 75 | [{"dataset " ++ integer_to_list(Ne), [{X, Fu(X)} || X <-lists:seq(Nb,Ne,10)]} || 76 | {{Nb, Ne}, Fu} <- Ns], 77 | [{"errorset " ++ integer_to_list(Ne), [{X, Fu(X), rand(20)} || X <-lists:seq(Nb,Ne,10)]} || 78 | {{Nb, Ne}, Fu} <- Ns] 79 | ], 80 | 81 | 82 | Tests = [{Function, Data, Option} || Function <- Functions, Option <- Options, Data <- Datas], 83 | 84 | lists:foldl(fun 85 | ({Function, Data, Opts}, I) -> 86 | io:format("[~3w] ~p with ~p~n", [I, Function, Opts]), 87 | Chart = egd_chart:Function(Data, Opts), 88 | egd:save(Chart, "example/" ++ atom_to_list(Function) ++ "_" ++ integer_to_list(I) ++ ".png"), 89 | I + 1 90 | end, 1, Tests), 91 | ok. 92 | 93 | 94 | ebin() -> 95 | filename:join([filename:dirname(escript:script_name()), "..", "ebin"]). 96 | 97 | 98 | rand(N) -> random:uniform(N). 99 | -------------------------------------------------------------------------------- /example/data1.dat: -------------------------------------------------------------------------------- 1 | 1 1 2 | 2 2 3 | 8 7 4 | 10 13 5 | -------------------------------------------------------------------------------- /example/data2.dat: -------------------------------------------------------------------------------- 1 | 1 10 2 | 2 8 3 | 8 3 4 | 9 1 5 | -------------------------------------------------------------------------------- /example/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyeugenic/eplot/5d0d45b0afd38f10d430e4547cea4fd82f444d7e/example/test1.png -------------------------------------------------------------------------------- /priv/eplot/priv/fonts/6x11_latin1.wingsfont: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyeugenic/eplot/5d0d45b0afd38f10d430e4547cea4fd82f444d7e/priv/eplot/priv/fonts/6x11_latin1.wingsfont -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% vim: syntax=erlang 2 | 3 | {erl_opts, [debug_info, fail_on_warning]}. 4 | 5 | {provider_hooks, [ 6 | {post, [{compile, {appup, compile}}, 7 | {compile, escriptize}, 8 | {clean, {appup, clean}}]} 9 | ]}. 10 | 11 | {cover_enabled, true}. 12 | {deps, [{egd, {git, "https://github.com/erlang/egd.git", {tag, "0.10.0"}}}]}. 13 | 14 | {escript_incl_apps,[egd,eplot]}. 15 | {escript_incl_extra, [{"eplot/priv/fonts/*.wingsfont", "priv"}]}. 16 | {escript_main_app, eplot}. 17 | {escript_name, eplot}. 18 | -------------------------------------------------------------------------------- /src/egd_chart.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: egd_chart.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2016-05-02 7 | %% 8 | 9 | -module(egd_chart). 10 | 11 | -export([graph/1, 12 | graph/2, 13 | bar2d/1, 14 | bar2d/2]). 15 | 16 | -export([smart_ticksize/3]). 17 | 18 | -type rgba() :: {byte(), byte(), byte(), byte()}. 19 | 20 | -record(chart, { 21 | type = png, 22 | render_engine = opaque, 23 | margin = 30 :: non_neg_integer(), % margin 24 | bbx = {{30,30}, {130,130}}, % Graph boundingbox (internal) 25 | ibbx = undefined, 26 | ranges = {{0,0}, {100,100}}, % Data boundingbox 27 | width = 160 :: non_neg_integer(), % Total chart width 28 | height = 160 :: non_neg_integer(), % Total chart height 29 | dxdy = {1.0,1.0}, 30 | ticksize = {10,10}, 31 | precision = {2,2}, 32 | 33 | % colors 34 | bg_rgba = {230, 230, 255, 255} :: rgba(), 35 | margin_rgba = {255, 255, 255, 255} :: rgba(), 36 | graph_rgba = [], % ordered color convention 37 | 38 | % graph specific 39 | x_label = "X" :: string() | atom(), 40 | y_label = "Y" :: string() | atom(), 41 | graph_name_yo = 10 :: integer(), % Name offset from top 42 | graph_name_yh = 10 :: integer(), % Name drop offset 43 | graph_name_xo = 10 :: integer(), % Name offset from RHS 44 | 45 | % bar2d specific 46 | bar_width = 40, 47 | column_width = 40}). 48 | 49 | -define(float_error, 0.0000000000000001). 50 | 51 | 52 | %% graph/1 and graph/2 53 | %% In: 54 | %% Data :: [{Graphname :: atom() | string(), [{X,Y}]}] 55 | %% Options :: [ 56 | %% % Metric options 57 | %% { width, integer() }, (300) 58 | %% { height, integer() }, (300) 59 | %% { margin, integer() }, (30) 60 | %% { ticksize, { integer(), integer() }, 61 | %% { x_range, { float(), float() }, 62 | %% { y_range, { float(), float() }, 63 | %% 64 | %% % Naming options 65 | %% { x_label, string() | atom() }, 66 | %% { y_label, string() | atom() }, 67 | %% 68 | %% % Color options 69 | %% {bg_rgba, {byte(), byte(), byte()}} 70 | %% ] 71 | graph(Data) -> graph(Data, [{width,300},{height,300}]). 72 | 73 | graph(Data, Options) -> 74 | Chart = graph_chart(Options,Data), 75 | Im = egd:create(Chart#chart.width, Chart#chart.height), 76 | LightBlue = egd:color(Chart#chart.bg_rgba), 77 | {Pt1, Pt2} = Chart#chart.bbx, 78 | egd:filledRectangle(Im, Pt1, Pt2, LightBlue), % background 79 | 80 | % Fonts? Check for text enabling 81 | 82 | Font = load_font("6x11_latin1.wingsfont"), 83 | 84 | draw_graphs(Data, Chart, Im), 85 | 86 | % estetic crop, necessary? 87 | {{X0,Y0}, {X1,Y1}} = Chart#chart.bbx, 88 | W = Chart#chart.width, 89 | H = Chart#chart.height, 90 | White = egd:color(Chart#chart.margin_rgba), 91 | egd:filledRectangle(Im, {0,0}, {X0-1,H}, White), 92 | egd:filledRectangle(Im, {X1+1,0}, {W,H}, White), 93 | egd:filledRectangle(Im, {0,0}, {W,Y0-1}, White), 94 | egd:filledRectangle(Im, {0,Y1+1}, {W,H}, White), 95 | 96 | draw_ticks(Chart, Im, Font), 97 | 98 | draw_origo_lines(Chart, Im), % draw origo crosshair 99 | 100 | draw_graph_names(Data, Chart, Font, Im), 101 | 102 | draw_xlabel(Chart, Im, Font), 103 | draw_ylabel(Chart, Im, Font), 104 | 105 | Png = egd:render(Im, Chart#chart.type, [{render_engine, Chart#chart.render_engine}]), 106 | egd:destroy(Im), 107 | try erlang:exit(Im, normal) catch _:_ -> ok end, 108 | Png. 109 | 110 | graph_chart(Opts, Data) -> 111 | {{X0,Y0}, 112 | {X1,Y1}} = proplists:get_value(ranges, Opts, ranges(Data)), 113 | Type = proplists:get_value(type, Opts, png), 114 | Width = proplists:get_value(width, Opts, 600), 115 | Height = proplists:get_value(height, Opts, 600), 116 | Xlabel = proplists:get_value(x_label, Opts, "X"), 117 | Ylabel = proplists:get_value(y_label, Opts, "Y"), 118 | %% multiple ways to set ranges 119 | XrangeMax = proplists:get_value(x_range_max, Opts, X1), 120 | XrangeMin = proplists:get_value(x_range_min, Opts, X0), 121 | YrangeMax = proplists:get_value(y_range_max, Opts, Y1), 122 | YrangeMin = proplists:get_value(y_range_min, Opts, Y0), 123 | {Xr0,Xr1} = proplists:get_value(x_range, Opts, {XrangeMin,XrangeMax}), 124 | {Yr0,Yr1} = proplists:get_value(y_range, Opts, {YrangeMin,YrangeMax}), 125 | Ranges = {{Xr0, Yr0}, {Xr1,Yr1}}, 126 | Precision = precision_level(Ranges, 10), 127 | {TsX,TsY} = smart_ticksize(Ranges, 10), 128 | XTicksize = proplists:get_value(x_ticksize, Opts, TsX), 129 | YTicksize = proplists:get_value(y_ticksize, Opts, TsY), 130 | Ticksize = proplists:get_value(ticksize, Opts, {XTicksize,YTicksize}), 131 | Margin = proplists:get_value(margin, Opts, 30), 132 | BGC = proplists:get_value(bg_rgba, Opts, {230,230, 255, 255}), 133 | Renderer = proplists:get_value(render_engine, Opts, opaque), 134 | 135 | BBX = {{Margin, Margin}, {Width - Margin, Height - Margin}}, 136 | DxDy = update_dxdy(Ranges,BBX), 137 | 138 | #chart{type = Type, 139 | width = Width, 140 | height = Height, 141 | x_label = Xlabel, 142 | y_label = Ylabel, 143 | ranges = Ranges, 144 | precision = Precision, 145 | ticksize = Ticksize, 146 | margin = Margin, 147 | bbx = BBX, 148 | dxdy = DxDy, 149 | render_engine = Renderer, 150 | bg_rgba = BGC}. 151 | 152 | draw_ylabel(Chart, Im, Font) -> 153 | Label = string(Chart#chart.y_label, 2), 154 | N = length(Label), 155 | {Fw,_Fh} = egd_font:size(Font), 156 | Width = N*Fw, 157 | {{Xbbx,Ybbx}, {_,_}} = Chart#chart.bbx, 158 | Pt = {Xbbx - trunc(Width/2), Ybbx - 20}, 159 | egd:text(Im, Pt, Font, Label, egd:color({0,0,0})). 160 | 161 | draw_xlabel(Chart, Im, Font) -> 162 | Label = string(Chart#chart.x_label, 2), 163 | N = length(Label), 164 | {Fw,_Fh} = egd_font:size(Font), 165 | Width = N*Fw, 166 | {{Xbbxl,_}, {Xbbxr,Ybbx}} = Chart#chart.bbx, 167 | Xc = trunc((Xbbxr - Xbbxl)/2) + Chart#chart.margin, 168 | Y = Ybbx + 20, 169 | Pt = {Xc - trunc(Width/2), Y}, 170 | egd:text(Im, Pt, Font, Label, egd:color({0,0,0})). 171 | 172 | draw_graphs(Datas, Chart, Im) -> 173 | draw_graphs(Datas, 0, Chart, Im). 174 | draw_graphs([],_,_,_) -> ok; 175 | draw_graphs([{_, Data}|Datas], ColorIndex, Chart, Im) -> 176 | Color = egd_colorscheme:select(default, ColorIndex), 177 | % convert data to graph data 178 | % fewer pass of xy2chart 179 | GraphData = [xy2chart(Pt, Chart) || Pt <- Data], 180 | draw_graph(GraphData, Color, Im), 181 | draw_graphs(Datas, ColorIndex + 1, Chart, Im). 182 | 183 | draw_graph([], _,_) -> ok; 184 | draw_graph([Pt1,Pt2|Data], Color, Im) -> 185 | draw_graph_dot(Pt1, Color, Im), 186 | draw_graph_line(Pt1,Pt2, Color, Im), 187 | draw_graph([Pt2|Data], Color, Im); 188 | 189 | draw_graph([Pt|Data], Color, Im) -> 190 | draw_graph_dot(Pt, Color, Im), 191 | draw_graph(Data, Color, Im). 192 | 193 | draw_graph_dot({X,Y}, Color, Im) -> 194 | egd:filledEllipse(Im, {X - 3, Y - 3}, {X + 3, Y + 3}, Color); 195 | draw_graph_dot({X,Y,Ey}, Color, Im) -> 196 | egd:line(Im, {X, Y - Ey}, {X, Y + Ey}, Color), 197 | egd:line(Im, {X - 4, Y - Ey}, {X + 4, Y - Ey}, Color), 198 | egd:line(Im, {X - 4, Y + Ey}, {X + 4, Y + Ey}, Color), 199 | egd:filledEllipse(Im, {X - 3, Y - 3}, {X + 3, Y + 3}, Color). 200 | 201 | draw_graph_line({X1,Y1,_},{X2,Y2,_}, Color, Im) -> 202 | egd:line(Im, {X1,Y1}, {X2,Y2}, Color); 203 | draw_graph_line(Pt1, Pt2, Color, Im) -> 204 | egd:line(Im, Pt1, Pt2, Color). 205 | 206 | %% name and color information 207 | 208 | 209 | draw_graph_names(Datas, Chart, Font, Im) -> 210 | draw_graph_names(Datas, 0, Chart, Font, Im, 0, Chart#chart.graph_name_yh). 211 | draw_graph_names([],_,_,_,_,_,_) -> ok; 212 | draw_graph_names([{Name, _}|Datas], ColorIndex, Chart, Font, Im, Yo, Yh) -> 213 | Color = egd_colorscheme:select(default, ColorIndex), 214 | draw_graph_name_color(Chart, Im, Font, Name, Color, Yo), 215 | draw_graph_names(Datas, ColorIndex + 1, Chart, Font, Im, Yo + Yh, Yh). 216 | 217 | draw_graph_name_color(#chart{bbx={{_,Y0},{X1,_}}}=Chart, Im, Font, Name, Color, Yh) -> 218 | Xo = Chart#chart.graph_name_xo, 219 | Yo = Chart#chart.graph_name_yo, 220 | Xl = 50, 221 | LPt1 = {X1 - Xo - Xl, Y0 + Yo + Yh}, 222 | LPt2 = {X1 - Xo, Y0 + Yo + Yh}, 223 | 224 | {Fw,Fh} = egd_font:size(Font), 225 | Str = string(Name,2), 226 | N = length(Str), 227 | TPt = {X1 - 2*Xo - Xl - Fw*N, Y0 + Yo + Yh - trunc(Fh/2) - 3}, 228 | 229 | egd:filledRectangle(Im, LPt1, LPt2, Color), 230 | egd:text(Im, TPt, Font, Str, egd:color({0,0,0})). 231 | 232 | %% origo crosshair 233 | 234 | draw_origo_lines(#chart{bbx={{X0,Y0},{X1,Y1}}}=Chart, Im) -> 235 | Black = egd:color({20,20,20}), 236 | Black1 = egd:color({50,50,50}), 237 | {X,Y} = xy2chart({0,0}, Chart), 238 | 239 | if X > X0, X < X1, Y > Y0, Y < Y1 -> 240 | egd:filledRectangle(Im, {X0,Y}, {X1,Y}, Black1), 241 | egd:filledRectangle(Im, {X,Y0}, {X,Y1}, Black1); 242 | true -> ok 243 | end, 244 | egd:rectangle(Im, {X0,Y0}, {X1,Y1}, Black), 245 | ok. 246 | 247 | % new ticks 248 | 249 | draw_ticks(Chart, Im, Font) -> 250 | {Xts, Yts} = Chart#chart.ticksize, 251 | {{Xmin,Ymin}, {Xmax,Ymax}} = Chart#chart.ranges, 252 | Ys = case Ymin of 253 | Ymin when Ymin < 0 -> trunc(Ymin/Yts) * Yts; 254 | _ -> (trunc(Ymin/Yts) + 1) * Yts 255 | end, 256 | Xs = case Xmin of 257 | Xmin when Xmin < 0 -> trunc(Xmin/Xts) * Xts; 258 | _ -> (trunc(Xmin/Xts) + 1) * Xts 259 | end, 260 | draw_yticks_lp(Im, Chart, Ys, Yts, Ymax, Font), 261 | draw_xticks_lp(Im, Chart, Xs, Xts, Xmax, Font). 262 | 263 | draw_yticks_lp(Im, Chart, Yi, Yts, Ymax, Font) when Yi < Ymax -> 264 | {_,Y} = xy2chart({0,Yi}, Chart), 265 | {{X,_}, _} = Chart#chart.bbx, 266 | {_, Precision} = Chart#chart.precision, 267 | draw_perf_ybar(Im, Chart, Y), 268 | egd:filledRectangle(Im, {X-2,Y}, {X+2,Y}, egd:color({0,0,0})), 269 | tick_text(Im, Font, Yi, {X,Y}, Precision, left), 270 | draw_yticks_lp(Im, Chart, Yi + Yts, Yts, Ymax, Font); 271 | draw_yticks_lp(_,_,_,_,_,_) -> ok. 272 | 273 | draw_xticks_lp(Im, Chart, Xi, Xts, Xmax, Font) when Xi < Xmax -> 274 | {X,_} = xy2chart({Xi,0}, Chart), 275 | {_, {_,Y}} = Chart#chart.bbx, 276 | { Precision, _} = Chart#chart.precision, 277 | draw_perf_xbar(Im, Chart, X), 278 | egd:filledRectangle(Im, {X,Y-2}, {X,Y+2}, egd:color({0,0,0})), 279 | tick_text(Im, Font, Xi, {X,Y}, Precision, below), 280 | draw_xticks_lp(Im, Chart, Xi + Xts, Xts, Xmax, Font); 281 | draw_xticks_lp(_,_,_,_,_,_) -> ok. 282 | 283 | tick_text(Im, Font, Tick, {X,Y}, Precision, Orientation) -> 284 | String = string(Tick, Precision), 285 | L = length(String), 286 | {Xl,Yl} = egd_font:size(Font), 287 | PxL = L*Xl, 288 | {Xo,Yo} = case Orientation of 289 | above -> {-round(PxL/2), -Yl - 3}; 290 | below -> {-round(PxL/2), 3}; 291 | left -> {round(-PxL - 4),-round(Yl/2) - 1}; 292 | right -> {3, -round(Yl/2)} 293 | end, 294 | egd:text(Im, {X + Xo,Y + Yo}, Font, String, egd:color({0,0,0})). 295 | 296 | % background tick bars, should be drawn with background 297 | 298 | draw_perf_ybar(Im, Chart, Yi) -> 299 | Pw = 5, 300 | Lw = 10, 301 | {{X0,_},{X1,_}} = Chart#chart.bbx, 302 | [Xl,Xr] = lists:sort([X0,X1]), 303 | Color = egd:color({180,180,190}), 304 | foreach_seq(fun(X) -> 305 | egd:filledRectangle(Im, {X,Yi}, {X+Pw, Yi}, Color) 306 | end, Xl,Xr,Lw), 307 | ok. 308 | 309 | draw_perf_xbar(Im, Chart, Xi) -> 310 | Pw = 5, 311 | Lw = 10, 312 | {{_,Y0},{_,Y1}} = Chart#chart.bbx, 313 | [Yu,Yl] = lists:sort([Y0,Y1]), 314 | Color = egd:color({130,130,130}), 315 | foreach_seq(fun(Y) -> 316 | egd:filledRectangle(Im, {Xi,Y}, {Xi, Y+Pw}, Color) 317 | end, Yu,Yl,Lw), 318 | ok. 319 | 320 | %% bar2d/1 and bar2d/2 321 | %% In: 322 | %% Data :: [{Datasetname :: string(), [{Keyname :: atom() | string(), 323 | %% Value :: number()}]}] 324 | %% Datasetname = Name of this dataset (the color name) 325 | %% Keyname = The name of each grouping 326 | %% Options :: [{Key, Value}] 327 | %% Key = bar_width 328 | %% Key = column_width 329 | %% Colors? 330 | %% Abstract: 331 | %% The graph is devided into column where each column have 332 | %% one or more bars. 333 | %% Each column is associated with a name. 334 | %% Each bar may have a secondary name (a key). 335 | 336 | bar2d(Data) -> bar2d(Data, [{width, 600}, {height, 600}]). 337 | 338 | bar2d(Data0, Options) -> 339 | {ColorMap, Data} = bar2d_convert_data(Data0), 340 | Chart = bar2d_chart(Options, Data), 341 | Im = egd:create(Chart#chart.width, Chart#chart.height), 342 | LightBlue = egd:color(Chart#chart.bg_rgba), 343 | {Pt1, Pt2} = Chart#chart.bbx, 344 | egd:filledRectangle(Im, Pt1, Pt2, LightBlue), % background 345 | 346 | % Fonts? Check for text enabling 347 | Font = load_font("6x11_latin1.wingsfont"), 348 | 349 | draw_bar2d_ytick(Im, Chart, Font), 350 | 351 | % Color map texts for sets 352 | draw_bar2d_set_colormap(Im, Chart, Font, ColorMap), 353 | 354 | % Draw bars 355 | draw_bar2d_data(Data, Chart, Font, Im), 356 | 357 | egd:rectangle(Im, Pt1, Pt2, egd:color({0,0,0})), 358 | Png = egd:render(Im, Chart#chart.type, [{render_engine, Chart#chart.render_engine}]), 359 | egd:destroy(Im), 360 | try erlang:exit(Im, normal) catch _:_ -> ok end, 361 | Png. 362 | 363 | % [{Dataset, [{Key, Value}]}] -> [{Key, [{Dataset, Value}]}] 364 | bar2d_convert_data(Data) -> bar2d_convert_data(Data, 0,{[], []}). 365 | bar2d_convert_data([], _, {ColorMap, Out}) -> {lists:reverse(ColorMap), lists:sort(Out)}; 366 | bar2d_convert_data([{Set, KVs}|Data], ColorIndex, {ColorMap, Out}) -> 367 | Color = egd_colorscheme:select(default, ColorIndex), 368 | bar2d_convert_data(Data, ColorIndex + 1, {[{Set,Color}|ColorMap], bar2d_convert_data_kvs(KVs, Set, Color, Out)}). 369 | 370 | bar2d_convert_data_kvs([], _,_, Out) -> Out; 371 | bar2d_convert_data_kvs([{Key, Value,_} | KVs], Set, Color, Out) -> 372 | bar2d_convert_data_kvs([{Key, Value} | KVs], Set, Color, Out); 373 | bar2d_convert_data_kvs([{Key, Value}|KVs], Set, Color, Out) -> 374 | case proplists:get_value(Key, Out) of 375 | undefined -> 376 | bar2d_convert_data_kvs(KVs, Set, Color, [{Key,[{{Color, Set}, Value}]}|Out]); 377 | DVs -> 378 | bar2d_convert_data_kvs(KVs, Set, Color, [{Key,[{{Color, Set}, Value}|DVs]}|proplists:delete(Key, Out)]) 379 | end. 380 | 381 | % beta color map, static allocated 382 | 383 | bar2d_chart(Opts, Data) -> 384 | Values = lists:foldl(fun ({_, DVs}, Out) -> 385 | Vs = [V || {_,V} <- DVs], 386 | Out ++ Vs 387 | end, [], Data), 388 | Type = proplists:get_value(type, Opts, png), 389 | Margin = proplists:get_value(margin, Opts, 30), 390 | Width = proplists:get_value(width, Opts, 600), 391 | Height = proplists:get_value(height, Opts, 600), 392 | XrangeMax = proplists:get_value(x_range_max, Opts, length(Data)), 393 | XrangeMin = proplists:get_value(x_range_min, Opts, 0), 394 | YrangeMax = proplists:get_value(y_range_max, Opts, lists:max(Values)), 395 | YrangeMin = proplists:get_value(y_range_min, Opts, 0), 396 | {Yr0,Yr1} = proplists:get_value(y_range, Opts, {YrangeMin, YrangeMax}), 397 | {Xr0,Xr1} = proplists:get_value(x_range, Opts, {XrangeMin, XrangeMax}), 398 | Ranges = proplists:get_value(ranges, Opts, {{Xr0, Yr0}, {Xr1,Yr1}}), 399 | Ticksize = proplists:get_value(ticksize, Opts, smart_ticksize(Ranges, 10)), 400 | Cw = proplists:get_value(column_width, Opts, {ratio, 0.8}), 401 | Bw = proplists:get_value(bar_width, Opts, {ratio, 1.0}), 402 | InfoW = proplists:get_value(info_box, Opts, 0), 403 | Renderer = proplists:get_value(render_engine, Opts, opaque), 404 | % colors 405 | BGC = proplists:get_value(bg_rgba, Opts, {230, 230, 255, 255}), 406 | MGC = proplists:get_value(margin_rgba, Opts, {255, 255, 255, 255}), 407 | 408 | % bounding box 409 | IBBX = {{Width - Margin - InfoW, Margin}, 410 | {Width - Margin, Height - Margin}}, 411 | BBX = {{Margin, Margin}, 412 | {Width - Margin - InfoW - 10, Height - Margin}}, 413 | DxDy = update_dxdy(Ranges, BBX), 414 | 415 | #chart{type = Type, 416 | margin = Margin, 417 | width = Width, 418 | height = Height, 419 | ranges = Ranges, 420 | ticksize = Ticksize, 421 | bbx = BBX, 422 | ibbx = IBBX, 423 | dxdy = DxDy, 424 | column_width = Cw, 425 | bar_width = Bw, 426 | margin_rgba = MGC, 427 | bg_rgba = BGC, 428 | render_engine = Renderer}. 429 | 430 | draw_bar2d_set_colormap(Im, Chart, Font, ColorMap) -> 431 | Margin = Chart#chart.margin, 432 | draw_bar2d_set_colormap(Im, Chart, Font, ColorMap, {Margin, 3}, Margin). 433 | 434 | draw_bar2d_set_colormap(_, _, _, [], _, _) -> ok; 435 | draw_bar2d_set_colormap(Im, Chart, Font, [{Set, Color}|ColorMap], {X, Y}, Margin) -> 436 | String = string(Set, 2), 437 | egd:text(Im, {X + 10, Y}, Font, String, egd:color({0,0,0})), 438 | egd:filledRectangle(Im, {X,Y+3}, {X+5, Y+8}, Color), 439 | draw_bar2d_set_colormap_step(Im, Chart, Font, ColorMap, {X,Y}, Margin). 440 | 441 | draw_bar2d_set_colormap_step(Im, Chart, Font, ColorMap, {X,Y}, Margin) when (Y + 23) < Margin -> 442 | draw_bar2d_set_colormap(Im, Chart, Font, ColorMap, {X, Y + 12}, Margin); 443 | draw_bar2d_set_colormap_step(Im, Chart, Font, ColorMap, {X,_Y}, Margin) -> 444 | draw_bar2d_set_colormap(Im, Chart, Font, ColorMap, {X + 144, 3}, Margin). 445 | 446 | draw_bar2d_ytick(Im, Chart, Font) -> 447 | {_, Yts} = Chart#chart.ticksize, 448 | {{_, _}, {_, Ymax}} = Chart#chart.ranges, 449 | draw_bar2d_yticks_up(Im, Chart, Yts, Yts, Ymax, Font). %% UPPER tick points 450 | 451 | draw_bar2d_yticks_up(Im, Chart, Yi, Yts, Ymax, Font) when Yi < Ymax -> 452 | {X, Y} = xy2chart({0,Yi}, Chart), 453 | {_, Precision} = Chart#chart.precision, 454 | draw_bar2d_ybar(Im, Chart, Y), 455 | egd:filledRectangle(Im, {X-2,Y}, {X+2,Y}, egd:color({0,0,0})), 456 | tick_text(Im, Font, Yi, {X,Y}, Precision, left), 457 | draw_bar2d_yticks_up(Im, Chart, Yi + Yts, Yts, Ymax, Font); 458 | draw_bar2d_yticks_up(_,_,_,_,_,_) -> ok. 459 | 460 | draw_bar2d_ybar(Im, Chart, Yi) -> 461 | Pw = 5, 462 | Lw = 10, 463 | {{X0,_},{X1,_}} = Chart#chart.bbx, 464 | [Xl,Xr] = lists:sort([X0,X1]), 465 | Color = egd:color({180,180,190}), 466 | foreach_seq(fun(X) -> 467 | egd:filledRectangle(Im, {X-Pw,Yi}, {X, Yi}, Color) 468 | end,Xl+Pw,Xr,Lw), 469 | ok. 470 | 471 | draw_bar2d_data(Columns, Chart, Font, Im) -> 472 | {{Xl,_}, {Xr,_}} = Chart#chart.bbx, 473 | Cn = length(Columns), % number of columns 474 | Co = (Xr - Xl)/(Cn), % column offset within chart 475 | Cx = Xl + Co/2, % start x of column 476 | draw_bar2d_data_columns(Columns, Chart, Font, Im, Cx, Co). 477 | 478 | draw_bar2d_data_columns([], _, _, _, _, _) -> ok; 479 | draw_bar2d_data_columns([{Name, Bars} | Columns], Chart, Font, Im, Cx, Co) -> 480 | {{_X0,_Y0}, {_X1,Y1}} = Chart#chart.bbx, 481 | 482 | Cwb = case Chart#chart.column_width of 483 | default -> Co; 484 | {ratio, P} when is_number(P) -> P*Co; 485 | Cw when is_number(Cw) -> lists:min([Cw,Co]) 486 | end, 487 | 488 | %% draw column text 489 | String = string(Name, 2), 490 | Ns = length(String), 491 | {Fw, Fh} = egd_font:size(Font), 492 | L = Fw*Ns, 493 | Tpt = {trunc(Cx - L/2 + 2), Y1 + Fh}, 494 | egd:text(Im, Tpt, Font, String, egd:color({0,0,0})), 495 | 496 | Bn = length(Bars), % number of bars 497 | Bo = Cwb/Bn, % bar offset within column 498 | Bx = Cx - Cwb/2 + Bo/2, % starting x of bar 499 | 500 | CS = 43, 501 | draw_bar2d_data_bars(Bars, Chart, Font, Im, Bx, Bo, CS), 502 | draw_bar2d_data_columns(Columns, Chart, Font, Im, Cx + Co, Co). 503 | 504 | draw_bar2d_data_bars([], _, _, _, _, _, _) -> ok; 505 | draw_bar2d_data_bars([{{Color,_Set}, Value}|Bars], Chart, Font, Im, Bx, Bo,CS) -> 506 | {{_X0,_Y0}, {_X1,Y1}} = Chart#chart.bbx, 507 | {_, Precision} = Chart#chart.precision, 508 | {_, Y} = xy2chart({0, Value}, Chart), 509 | 510 | Bwb = case Chart#chart.bar_width of 511 | default -> Bo; 512 | {ratio, P} when is_number(P) -> P*Bo; 513 | Bw when is_number(Bw) -> lists:min([Bw,Bo]) 514 | end, 515 | 516 | 517 | Black = egd:color({0,0,0}), 518 | 519 | % draw bar text 520 | String = string(Value, Precision), 521 | Ns = length(String), 522 | {Fw, Fh} = egd_font:size(Font), 523 | L = Fw*Ns, 524 | Tpt = {trunc(Bx - L/2 + 2), Y - Fh - 5}, 525 | egd:text(Im, Tpt, Font, String, Black), 526 | 527 | 528 | Pt1 = {trunc(Bx - Bwb/2), Y}, 529 | Pt2 = {trunc(Bx + Bwb/2), Y1}, 530 | egd:filledRectangle(Im, Pt1, Pt2, Color), 531 | egd:rectangle(Im, Pt1, Pt2, Black), 532 | draw_bar2d_data_bars(Bars, Chart, Font, Im, Bx + Bo, Bo, CS + CS). 533 | 534 | %%========================================================================== 535 | %% 536 | %% Aux functions 537 | %% 538 | %%========================================================================== 539 | 540 | 541 | xy2chart({X,Y}, #chart{ranges = {{Rx0,Ry0}, {_Rx1,_Ry1}}, 542 | bbx = {{Bx0,By0}, {_Bx1, By1}}, 543 | dxdy = {Dx, Dy}, 544 | margin = Margin}) -> 545 | {round(X*Dx + Bx0 - Rx0*Dx), round(By1 - (Y*Dy + By0 - Ry0*Dy - Margin))}; 546 | xy2chart({X,Y,Error}, Chart) -> 547 | {Xc,Yc} = xy2chart({X,Y}, #chart{ dxdy = {_,Dy} } = Chart), 548 | {Xc, Yc, round(Dy*Error)}. 549 | 550 | ranges([{_Name, Es}|Data]) when is_list(Es) -> 551 | Ranges = xy_minmax(Es), 552 | ranges(Data, Ranges). 553 | 554 | ranges([], Ranges) -> Ranges; 555 | ranges([{_Name, Es}|Data], CoRanges) when is_list(Es) -> 556 | Ranges = xy_minmax(Es), 557 | ranges(Data, xy_resulting_ranges(Ranges, CoRanges)). 558 | 559 | 560 | smart_ticksize({{X0, Y0}, {X1, Y1}}, N) -> 561 | {smart_ticksize(X0,X1,N), smart_ticksize(Y0,Y1,N)}. 562 | 563 | 564 | smart_ticksize(S, E, N) when is_number(S), is_number(E), is_number(N) -> 565 | % Calculate stepsize then 'humanize' the value to a human pleasing format. 566 | R = abs((E - S))/N, 567 | if abs(R) < ?float_error -> 2.0; 568 | true -> 569 | % get the ratio on the form of 2-3 significant digits. 570 | P = precision_level(S, E, N), 571 | M = math:pow(10, P), 572 | Vsig = R*M, 573 | %% do magic 574 | Rsig = Vsig/50, 575 | Hsig = 50 * trunc(Rsig + 0.5), 576 | %% fin magic 577 | Hsig/M 578 | end; 579 | smart_ticksize(_, _, _) -> 2.0. 580 | 581 | precision_level({{X0, Y0}, {X1, Y1}}, N) -> 582 | { precision_level(X0,X1,N), precision_level(Y0,Y1,N)}. 583 | 584 | precision_level(S, E, N) when is_number(S), is_number(E) -> 585 | % Calculate stepsize then 'humanize' the value to a human pleasing format. 586 | R = abs((E - S))/N, 587 | if abs(R) < ?float_error -> 2; 588 | true -> 589 | % get the ratio on the form of 2-3 significant digits. 590 | V = 2 - math:log10(R), 591 | trunc(V + 0.5) 592 | end; 593 | precision_level(_, _, _) -> 2. 594 | 595 | % on form [{X,Y}] | [{X,Y,E}] 596 | xy_minmax(Elements) -> 597 | {Xs, Ys} = lists:foldl(fun ({X,Y,_},{Xis, Yis}) -> {[X|Xis],[Y|Yis]}; 598 | ({X,Y}, {Xis, Yis}) -> {[X|Xis],[Y|Yis]} 599 | end, {[],[]}, Elements), 600 | {{lists:min(Xs),lists:min(Ys)},{lists:max(Xs), lists:max(Ys)}}. 601 | 602 | xy_resulting_ranges({{X0,Y0},{X1,Y1}},{{X2,Y2},{X3,Y3}}) -> 603 | {{lists:min([X0,X1,X2,X3]),lists:min([Y0,Y1,Y2,Y3])}, 604 | {lists:max([X0,X1,X2,X3]),lists:max([Y0,Y1,Y2,Y3])}}. 605 | 606 | update_dxdy({{Rx0, Ry0}, {Rx1, Ry1}}, {{Bx0,By0},{Bx1,By1}}) -> 607 | Dx = divide((Bx1 - Bx0),(Rx1 - Rx0)), 608 | Dy = divide((By1 - By0),(Ry1 - Ry0)), 609 | {Dx,Dy}. 610 | 611 | divide(_T,N) when abs(N) < ?float_error -> 0.0; 612 | divide(T,N) -> T/N. 613 | 614 | %print_info_chart(Chart) -> 615 | % io:format("Chart ->~n"), 616 | % io:format(" type: ~p~n", [Chart#chart.type]), 617 | % io:format(" margin: ~p~n", [Chart#chart.margin]), 618 | % io:format(" bbx: ~p~n", [Chart#chart.bbx]), 619 | % io:format(" ticksize: ~p~n", [Chart#chart.ticksize]), 620 | % io:format(" ranges: ~p~n", [Chart#chart.ranges]), 621 | % io:format(" width: ~p~n", [Chart#chart.width]), 622 | % io:format(" height: ~p~n", [Chart#chart.height]), 623 | % io:format(" dxdy: ~p~n", [Chart#chart.dxdy]), 624 | % ok. 625 | 626 | string(E, _P) when is_atom(E) -> atom_to_list(E); 627 | string(E, P) when is_float(E) -> float_to_maybe_integer_to_string(E, P); 628 | string(E, _P) when is_integer(E) -> s("~w", [E]); 629 | string(E, _P) when is_binary(E) -> lists:flatten(binary_to_list(E)); 630 | string(E, _P) when is_list(E) -> s("~s", [E]). 631 | 632 | float_to_maybe_integer_to_string(F, P) -> 633 | I = trunc(F), 634 | A = abs(I - F), 635 | if A < ?float_error -> s("~w", [I]); % integer 636 | true -> s(s("~~.~wf", [P]), [F]) % float 637 | end. 638 | 639 | s(Format, Terms) -> 640 | lists:flatten(io_lib:format(Format, Terms)). 641 | 642 | foreach_seq(Fun, First, Last, Inc) -> 643 | if 644 | Inc > 0, First - Inc =< Last; 645 | Inc < 0, First - Inc >= Last -> 646 | N = (Last - First + Inc) div Inc, 647 | foreach_seq_loop(N, First, Inc, Fun); 648 | First =:= Last -> 649 | foreach_seq_loop(1, First, Inc, Fun) 650 | end. 651 | 652 | foreach_seq_loop(0, _, _, _) -> ok; 653 | foreach_seq_loop(N, I, Inc, Fun) -> 654 | _ = Fun(I), 655 | foreach_seq_loop(N-1, I+Inc, Inc, Fun). 656 | 657 | load_font(Font) -> 658 | case erl_prim_loader:get_file(filename:join([code:priv_dir(eplot),"fonts",Font])) of 659 | {ok,FontBinary,_} -> 660 | %% archive 661 | egd_font:load_binary(FontBinary); 662 | _ -> 663 | {ok,FontBinary,_} = erl_prim_loader:get_file(filename:join([code:priv_dir(eplot),"eplot/priv/fonts",Font])), 664 | egd_font:load_binary(FontBinary) 665 | end. 666 | -------------------------------------------------------------------------------- /src/egd_colorscheme.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2011 Björn-Egil Dahlberg 2 | %% 3 | %% File: egd_colorscheme.erl 4 | %% Author: Björn-Egil Dahlberg 5 | %% Created: 2011-09-02 6 | 7 | -module(egd_colorscheme). 8 | 9 | -export([ 10 | hsl2rgb/1, 11 | rgb2hsl/1, 12 | select/2 13 | ]). 14 | 15 | % L - low dark, high bright 16 | % S 17 | 18 | select(core1, I) -> select(I, 17, 0.3, 0.3, 200); 19 | select(_Default, I) -> select(I + 6, 31, 0.8, 0.4, 210). 20 | select(I, Hm, S, L, A) -> egd:color(hsl2rgb({I*Hm rem 360, S, L, A})). 21 | 22 | %% color conversions 23 | %% H, hue has the range of [0, 360] 24 | %% S, saturation has the range of [0,1] 25 | %% L, lightness has the range of [0,1] 26 | 27 | -define(float_error, 0.000000000001). 28 | 29 | hsl2rgb({H,S,L}) -> hsl2rgb({H,S,L,255}); 30 | hsl2rgb({H,S,L,A}) -> 31 | Q = if 32 | L < 0.5 -> L * (1 + S); 33 | true -> L + S - (L * S) 34 | end, 35 | P = 2 * L - Q, 36 | Hk = H/360, 37 | Rt = Hk + 1/3, 38 | Gt = Hk, 39 | Bt = Hk - 1/3, 40 | 41 | Cts = lists:map(fun 42 | (Tc) when Tc < 0.0 -> Tc + 1.0; 43 | (Tc) when Tc > 1.0 -> Tc - 1.0; 44 | (Tc) -> Tc 45 | end, [Rt, Gt, Bt]), 46 | [R,G,B] = lists:map(fun 47 | (Tc) when Tc < 1/6 -> P + ((Q - P) * 6 * Tc); 48 | (Tc) when Tc < 1/2, Tc >= 1/6 -> Q; 49 | (Tc) when Tc < 2/3, Tc >= 1/2 -> P + ((Q - P) * 6 * (2/3 - Tc)); 50 | (_ ) -> P 51 | end, Cts), 52 | {trunc(R*255),trunc(G*255),trunc(B*255),A}. 53 | 54 | byte2float(B) -> envelope(B/255, 0, 1). 55 | 56 | envelope(V, Min, Max) when V >= Min andalso V =< Max -> V; 57 | envelope(V, Min, Max) when V < Min andalso V < Max -> Min; 58 | envelope(V, Min, Max) when V > Min andalso V > Max -> Max. 59 | 60 | rgb2hsl({R,G,B}) -> rgb2hsl({R,G,B,255}); 61 | rgb2hsl({R,G,B,A}) -> 62 | Rf = byte2float(R), 63 | Gf = byte2float(G), 64 | Bf = byte2float(B), 65 | 66 | Max = lists:max([Rf,Gf,Bf]), 67 | Min = lists:min([Rf,Gf,Bf]), 68 | H = if 69 | abs(Max - Min) < ?float_error -> 70 | 0.0; 71 | abs(Max - Rf) < ?float_error -> 72 | D = 60 * (Gf - Bf)/(Max - Min), 73 | Dt = trunc(D), 74 | case {((Dt + 360) rem 360), (D - Dt)} of 75 | {0, Frac} when Frac < 0 -> 360.0 + Frac; 76 | {Degs, Frac} -> Degs + Frac 77 | end; 78 | abs(Max - Gf) < ?float_error -> 79 | 60 * (Bf - Rf)/(Max - Min) + 120; 80 | abs(Max - Bf) < ?float_error -> 81 | 60 * (Rf - Gf)/(Max - Min) + 240; 82 | true -> 83 | 0.0 84 | end, 85 | L = (Max + Min)/2, 86 | S = if 87 | abs(Max - Min) < ?float_error -> 88 | 0; 89 | L > 0.5 -> 90 | (Max - Min)/(2 - (Max + Min)); 91 | true -> 92 | (Max - Min)/(Max + Min) 93 | end, 94 | {H, envelope(S, 0.0, 1.0), envelope(L, 0.0, 1.0), A}. 95 | -------------------------------------------------------------------------------- /src/eplot.app.src: -------------------------------------------------------------------------------- 1 | {application, eplot, [ 2 | {description, "A plot engine and drawer"}, 3 | {vsn, git}, 4 | {modules, [ 5 | egd_colorscheme, 6 | egd_chart, 7 | eplot_main, 8 | eplot_view 9 | ]}, 10 | {registered, []}, 11 | {applications, [ 12 | kernel, 13 | stdlib, 14 | egd, 15 | wx 16 | ]} 17 | ]}. 18 | 19 | %% vim: ft=erlang 20 | -------------------------------------------------------------------------------- /src/eplot.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: eplot.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2012-11-22 7 | %% 8 | 9 | -module(eplot). 10 | 11 | -export([main/1]). 12 | -mode(compile). 13 | 14 | usage() -> 15 | Text = 16 | "eplot [options] files\r\n" 17 | "Options:\r\n" 18 | "\t-o Outfile, :: filename(), directs the graph to the outfile\r\n" 19 | "\t-render_engine Engine :: alpha | opaque, type of render engine\r\n" 20 | "\t-type Type :: png | raw_bitmap, output type\r\n" 21 | "\t-plot Plot :: plot2d | bar2d, plot type\r\n" 22 | "\t-norm File :: filename(), normalize against\r\n" 23 | "\t-width Width :: integer(), Width\r\n" 24 | "\t-height Height :: integer(), Height\r\n" 25 | "\t-x_label Label :: string(), X-axis label\r\n" 26 | "\t-y_label Label :: string(), Y-axis label\r\n" 27 | 28 | "\t-x_range_min Min :: number()\r\n" 29 | "\t-x_range_max Max :: number()\r\n" 30 | "\t-y_range_min Min :: number()\r\n" 31 | "\t-y_range_max Max :: number()\r\n" 32 | "\t-margin Margin :: number()\r\n" 33 | "\t-x_ticksize Size :: number()\r\n" 34 | "\t-y_ticksize Size :: number()\r\n" 35 | "\t-bg_rgba RGB[A] :: Background, ex. fefefe[fe]\r\n", 36 | io:format("~s~n", [Text]), 37 | halt(), 38 | ok. 39 | 40 | main([]) -> usage(); 41 | main(Args) -> 42 | Options = parse_args(Args), 43 | code:add_patha(ebin()), 44 | Input = proplists:get_value(input, Options, []), 45 | case proplists:get_value(o, Options) of 46 | undefined -> eplot_main:view(Input, Options); 47 | Output -> eplot_main:png(Input, Output, Options) 48 | end. 49 | 50 | parse_args(Args) -> 51 | parse_args(Args, [{bg_rgba, {255,255,255,255}},{plot, plot2d}], []). 52 | parse_args([], Opts, Inputs) -> [{input, Inputs}|Opts]; 53 | parse_args([V0,V1|Args], Opts, Inputs) -> 54 | case V0 of 55 | "-h" -> 56 | usage(); 57 | "-speedup" -> 58 | parse_args([V1|Args], [{speedup,true}|Opts], Inputs); 59 | "-bg_rgba" -> 60 | parse_args(Args, [{bg_rgba, hexstring2rgb(V1)}|proplists:delete(bg_rgba, Opts)], Inputs); 61 | "-" ++ SKey-> 62 | Key = list_to_atom(SKey), 63 | parse_args(Args, [{Key, string_to_value(V1)}|proplists:delete(Key, Opts)], Inputs); 64 | _ -> 65 | parse_args([V1|Args], Opts, [V0|Inputs]) 66 | end; 67 | parse_args([Value|Args], Opts, Inputs) -> 68 | case Value of 69 | "-h" -> 70 | usage(); 71 | _ -> 72 | parse_args(Args, Opts, [Value|Inputs]) 73 | end. 74 | 75 | hexstring2rgb(Hs = [_R0,_R1,_G0,_G1,_B0,_B1]) -> hexstring2rgb(Hs ++ "FF"); 76 | hexstring2rgb([R0,R1,G0,G1,B0,B1,A0,A1]) -> 77 | R = hex2byte(R0)*15 + hex2byte(R1), 78 | G = hex2byte(G0)*15 + hex2byte(G1), 79 | B = hex2byte(B0)*15 + hex2byte(B1), 80 | A = hex2byte(A0)*15 + hex2byte(A1), 81 | {R,G,B,A}; 82 | hexstring2rgb(_) -> {255,255,255,255}. 83 | 84 | hex2byte(H) when H >= 48, H =< 57 -> H - 48; 85 | hex2byte(H) when H >= 65, H =< 70 -> H - 65 + 10; 86 | hex2byte(H) when H >= 97, H =< 102 -> H - 97 + 10; 87 | hex2byte(_) -> 0. 88 | 89 | string_to_value(Value) -> 90 | try 91 | list_to_integer(Value) 92 | catch 93 | _:_ -> 94 | try 95 | list_to_float(Value) 96 | catch 97 | _:_ -> 98 | list_to_atom(Value) 99 | end 100 | end. 101 | 102 | ebin() -> 103 | Sname = escript:script_name(), 104 | Rname = real_name(Sname), 105 | filename:join([filename:dirname(Rname),"..","ebin"]). 106 | 107 | real_name(Sname) -> real_name(Sname, file:read_link(Sname)). 108 | real_name(_, {ok, Rname}) -> real_name(Rname, file:read_link(Rname)); 109 | real_name(Rname, {error, einval}) -> Rname. 110 | -------------------------------------------------------------------------------- /src/eplot_main.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: eplot_main.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2016-05-02 7 | %% 8 | 9 | -module(eplot_main). 10 | -export([png/3, view/2]). 11 | 12 | png(Inputs, Output, Options0) -> 13 | Options = merge_options(Options0, get_config()), 14 | Data = process_data(parse_data_files(Inputs), Options), 15 | B = graph_binary(proplists:get_value(plot, Options), Data, Options), 16 | egd:save(B, Output), 17 | ok. 18 | 19 | view(Inputs, Options0) -> 20 | Options = proplists:delete(type, merge_options(Options0, get_config())), 21 | Data = process_data(parse_data_files(Inputs), Options), 22 | W = proplists:get_value(width, Options), 23 | H = proplists:get_value(height, Options), 24 | P = eplot_view:start({W,H}), 25 | B = graph_binary(proplists:get_value(plot, Options), Data, [{type, raw_bitmap}] ++ Options), 26 | P ! {self(), bmp_image, B, {W,H}}, 27 | receive {P, done} -> ok end. 28 | 29 | process_data(Data0, Options) -> 30 | Data1 = case proplists:is_defined(speedup, Options) of 31 | true -> data_speedup(Data0); 32 | false -> Data0 33 | end, 34 | case proplists:is_defined(norm, Options) of 35 | true -> 36 | [{_Name,Norm}] = parse_data_files([proplists:get_value(norm, Options)]), 37 | normalize_data(Data1, Norm); 38 | false -> 39 | Data1 40 | end. 41 | 42 | normalize_data([], _) -> []; 43 | normalize_data([{Name,Data}|Datas], Norm) -> 44 | [{Name, normalize_data_entries(Name, Data, Norm)}|normalize_data(Datas, Norm)]. 45 | normalize_data_entries(_, [], _) -> []; 46 | normalize_data_entries(Name, [{X,Y}|Entries], Norm) -> 47 | case proplists:get_value(X, Norm) of 48 | undefined -> 49 | io:format(standard_error, "Warning: entry ~p not in data file ~p\r\n", [X, Name]), 50 | normalize_data_entries(Name, Entries, Norm); 51 | Value -> 52 | [{X, Y/Value}|normalize_data_entries(Name, Entries, Norm)] 53 | end. 54 | 55 | graph_binary(plot2d,Data, Options) -> 56 | egd_chart:graph(Data, Options); 57 | graph_binary(bar2d, Data, Options) -> 58 | egd_chart:bar2d(Data, Options); 59 | graph_binary(Type, _, _) -> 60 | io:format("Bad engine: ~p~n", [Type]), 61 | exit(bad_plot_engine). 62 | 63 | parse_data_files([]) -> []; 64 | parse_data_files([Filename|Filenames]) -> 65 | Data = parse_data_file(Filename), 66 | Name = filename:basename(Filename), 67 | [{Name, Data}|parse_data_files(Filenames)]. 68 | 69 | 70 | merge_options([], Out) -> Out; 71 | merge_options([{Key, Value}|Opts], Out) -> 72 | merge_options(Opts, [{Key, Value}|proplists:delete(Key, Out)]). 73 | 74 | data_speedup([]) -> []; 75 | data_speedup([{Filename,[{X,Y}|T]}|Data]) -> 76 | Speedup = data_speedup(T, Y, [{X,1}]), 77 | [{Filename, Speedup}|data_speedup(Data)]. 78 | 79 | 80 | data_speedup([], _, Out) -> lists:reverse(Out); 81 | data_speedup([{X,Y}|T], F, Out) -> data_speedup(T, F, [{X,F/Y}|Out]). 82 | 83 | parse_data_file(Filename) -> 84 | {ok, Fd} = file:open(Filename, [read]), 85 | parse_data_file(Fd, io:get_line(Fd, ""), []). 86 | 87 | parse_data_file(Fd, eof, Out) -> file:close(Fd), lists:reverse(Out); 88 | parse_data_file(Fd, String, Out) -> 89 | % expected string is 'number()number()' 90 | Tokens = string:tokens(String, " \t\n\r"), 91 | Item = tokens2item(Tokens), 92 | parse_data_file(Fd, io:get_line(Fd, ""), [Item|Out]). 93 | 94 | tokens2item(Tokens) -> 95 | case lists:map(fun (String) -> string_to_term(String) end, Tokens) of 96 | [X,Y] -> {X,Y}; 97 | [X,Y,E|_] -> {X,Y,E} 98 | end. 99 | 100 | string_to_term(Value) -> 101 | try 102 | list_to_integer(Value) 103 | catch 104 | _:_ -> 105 | try 106 | list_to_float(Value) 107 | catch 108 | _:_ -> 109 | list_to_atom(Value) 110 | end 111 | end. 112 | 113 | 114 | get_config() -> 115 | Home = os:getenv("HOME"), 116 | Path = filename:join([Home, ".eplot"]), 117 | File = filename:join([Path, "eplot.config"]), 118 | case file:consult(File) of 119 | {ok, Terms} -> Terms; 120 | {error, enoent} -> make_config(Path, File) 121 | end. 122 | 123 | make_config(Path, File) -> 124 | Defaults = [{width, 1024}, {height, 800}], 125 | try 126 | file:make_dir(Path), 127 | {ok, Fd} = file:open(File, [write]), 128 | [io:format(Fd, "~p.~n", [Opt]) || Opt <- Defaults], 129 | file:close(Fd), 130 | Defaults 131 | catch 132 | A:B -> 133 | io:format("Error writing config. ~p ~p~n", [A,B]), 134 | Defaults 135 | end. 136 | -------------------------------------------------------------------------------- /src/eplot_view.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: eplot_view.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2016-05-02 7 | %% 8 | 9 | -module(eplot_view). 10 | 11 | -include_lib("wx/include/wx.hrl"). 12 | 13 | -export([start/0, start/1]). 14 | 15 | -record(s, {f,w, bin, bin_size, bmp, image, notify}). 16 | 17 | start() -> 18 | start({1024,800}). 19 | 20 | start(Size) -> 21 | spawn_link(fun() -> init(Size) end). 22 | 23 | init(Size) -> 24 | P = wx:new(), 25 | F = wxFrame:new(P,1, "Eplot View", [{size, Size}]), 26 | Opts = [{size, {1024, 800}}, {style, ?wxSUNKEN_BORDER}], 27 | W = wxWindow:new(F, ?wxID_ANY, Opts), 28 | wxFrame:connect(F, close_window, [{skip,true}]), 29 | wxWindow:connect(W, paint, [{skip, true}]), 30 | wxFrame:show(F), 31 | wxFrame:centre(F), 32 | loop(#s{f = F, w = W}). 33 | 34 | loop(S) -> 35 | receive 36 | _E=#wx{event=#wxPaint{}} -> 37 | redraw(S), 38 | loop(S); 39 | _E=#wx{id=EXIT, event=EV} when EXIT =:= ?wxID_EXIT; 40 | is_record(EV, wxClose) -> 41 | catch wxWindow:'Destroy'(S#s.f), 42 | S#s.notify ! {self(), done}, 43 | ok; 44 | {Pid, bmp_image, Bin, {W,H}} -> 45 | S1 = bin2bmp(S#s{bin = Bin, bin_size = {W,H}}), 46 | wxWindow:setClientSize(S#s.w, W, H), 47 | redraw(S1), 48 | wxWindow:setFocus(S#s.w), 49 | loop(S1#s{notify = Pid}); 50 | _ -> 51 | loop(S) 52 | end. 53 | 54 | bin2bmp(State) -> 55 | {W,H} = State#s.bin_size, 56 | Image = wxImage:new(W,H,State#s.bin), 57 | Bmp = wxBitmap:new(Image), 58 | State#s{bmp = Bmp, image = Image}. 59 | 60 | redraw(#s{w = Win, bmp = undefined}) -> 61 | DC0 = wxClientDC:new(Win), 62 | DC = wxBufferedDC:new(DC0), 63 | wxDC:clear(DC), 64 | wxBufferedDC:destroy(DC), 65 | wxClientDC:destroy(DC0), 66 | ok; 67 | redraw(#s{w = Win, bmp = Bmp}) -> 68 | DC0 = wxClientDC:new(Win), 69 | DC = wxBufferedDC:new(DC0), 70 | wxDC:clear(DC), 71 | wxDC:drawBitmap(DC,Bmp, {0,0}), 72 | wxBufferedDC:destroy(DC), 73 | wxClientDC:destroy(DC0), 74 | ok. 75 | -------------------------------------------------------------------------------- /test/egd_bar2d_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: egd_bar2d_SUITE.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2016-05-02 7 | %% 8 | 9 | -module(egd_bar2d_SUITE). 10 | -include_lib("common_test/include/ct.hrl"). 11 | 12 | %% callbacks 13 | 14 | -export([all/0, suite/0]). 15 | 16 | -export([bar2d_api/1]). 17 | 18 | suite() -> 19 | [{timetrap,{seconds,180}}]. 20 | 21 | all() -> 22 | [bar2d_api]. 23 | 24 | bar2d_api(_Config) -> 25 | D1 = [{"graph 1", make_simple_set(0, 100, 12)}], 26 | ok = check_chart_result(egd_chart:bar2d(D1)), 27 | 28 | D2 = [{"graph 2", make_simple_set(-100, 100, 13)}|D1], 29 | ok = check_chart_result(egd_chart:bar2d(D2)), 30 | 31 | D3 = [{"graph 3", make_simple_set(-1000, 1000, 30)}|D2], 32 | ok = check_chart_result(egd_chart:bar2d(D3)), 33 | 34 | D4 = [{graph_4, make_simple_set(-1000, 1000, 3)}|D3], 35 | ok = check_chart_result(egd_chart:bar2d(D4)), 36 | 37 | D5 = [{graph_5, make_simple_set(-1000, 10000, 111)}|D4], 38 | ok = check_chart_result(egd_chart:bar2d(D5)), 39 | 40 | D6 = [{graph_6, make_simple_set(-1000, 10000, 57)}], 41 | ok = check_chart_result(egd_chart:bar2d(D6)), 42 | 43 | D7 = [{"graph 7", make_simple_set(-300, -10, 1)}|D6], 44 | ok = check_chart_result(egd_chart:bar2d(D7)), 45 | ok. 46 | 47 | check_chart_result(B) when is_binary(B) -> ok. 48 | 49 | make_simple_set(Min, Max, Step) -> 50 | [{"bar " ++ integer_to_list(X),30*math:sin(X) + 30} || X <- lists:seq(Min,Max,Step)]. 51 | -------------------------------------------------------------------------------- /test/egd_chart_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: egd_chart_SUITE.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2012-11-24 7 | %% 8 | 9 | -module(egd_chart_SUITE). 10 | -include_lib("common_test/include/ct.hrl"). 11 | 12 | %% callbacks 13 | 14 | -export([all/0, suite/0]). 15 | 16 | -export([graph_api/1, 17 | graph_api_opts/1]). 18 | 19 | suite() -> 20 | [{timetrap,{seconds,180}}]. 21 | 22 | all() -> 23 | [graph_api, graph_api_opts]. 24 | 25 | graph_api(_Config) -> 26 | D1 = [{"graph 1", make_simple_set(0, 100, 12)}], 27 | ok = check_chart_result(egd_chart:graph(D1)), 28 | 29 | D2 = [{"graph 2", make_simple_set(-100, 100, 13)}|D1], 30 | ok = check_chart_result(egd_chart:graph(D2)), 31 | 32 | D3 = [{"graph 3", make_simple_set(-1000, 1000, 30)}|D2], 33 | ok = check_chart_result(egd_chart:graph(D3)), 34 | 35 | D4 = [{graph_4, make_simple_set(-1000, 1000, 3)}|D3], 36 | ok = check_chart_result(egd_chart:graph(D4)), 37 | 38 | D5 = [{graph_5, make_simple_set(-1000, 10000, 111)}|D4], 39 | ok = check_chart_result(egd_chart:graph(D5)), 40 | 41 | D6 = [{graph_6, make_simple_set(-1000, 10000, 57)}], 42 | ok = check_chart_result(egd_chart:graph(D6)), 43 | 44 | D7 = [{"graph 7", make_simple_set(-300, -10, 1)}|D6], 45 | ok = check_chart_result(egd_chart:graph(D7)), 46 | ok. 47 | 48 | graph_api_opts(_Config) -> 49 | Options = [ 50 | [{width, Width}, {height, Height}, {margin, Margin}, 51 | {ticksize, Ticksize}, {x_range, Xrange}, {y_range, Yrange}, 52 | {y_label, Ylabel}, {x_label, Xlabel}, {bg_rgba, Rgba}] || 53 | 54 | Width <- [400, 1300], 55 | Height <- [400, 1024], 56 | Margin <- [60], 57 | Ticksize <- [{12,12}, {180,180}], 58 | Xrange <- [{0, 100}, {-1000, 1000}], 59 | Yrange <- [{0, 100}, {-1000, 1000}], 60 | Ylabel <- ["Y-label"], 61 | Xlabel <- ["X-label"], 62 | Rgba <- [{255,255,255}] 63 | ], 64 | D1 = [{"graph 1", make_simple_set(0, 1200, 360)}], 65 | ok = check_graph_api_opts(D1, Options), 66 | ok. 67 | 68 | check_graph_api_opts(_, []) -> ok; 69 | check_graph_api_opts(Data, [Opts|Os]) -> 70 | T0 = os:timestamp(), 71 | ok = check_chart_result(egd_chart:graph(Data, Opts)), 72 | T1 = os:timestamp(), 73 | io:format("graph ~.2f s~n", [timer:now_diff(T1,T0) / 1000000]), 74 | check_graph_api_opts(Data, Os). 75 | 76 | check_chart_result(B) when is_binary(B) -> ok. 77 | 78 | make_simple_set(Min, Max, Step) -> 79 | [{X,30*math:sin(X)} || X <- lists:seq(Min,Max,Step)]. 80 | -------------------------------------------------------------------------------- /test/egd_colorscheme_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2016 Björn-Egil Dahlberg 3 | %% 4 | %% File: egd_colorscheme_SUITE.erl 5 | %% Author: Björn-Egil Dahlberg 6 | %% Created: 2012-11-25 7 | %% 8 | -module(egd_colorscheme_SUITE). 9 | 10 | -include_lib("common_test/include/ct.hrl"). 11 | 12 | %% callbacks 13 | 14 | -export([suite/0, all/0]). 15 | 16 | -export([hsl2rgb/1, 17 | rgb2hsl/1]). 18 | 19 | suite() -> 20 | [{timetrap,{seconds,180}}]. 21 | 22 | all() -> 23 | [hsl2rgb, rgb2hsl]. 24 | 25 | check_rgb({R, G, B, _}) -> check_rgb({R, G, B}); 26 | check_rgb({R, G, B}) when 27 | R >= 0.0 andalso R =< 255.0 andalso 28 | G >= 0.0 andalso G =< 255.0 andalso 29 | B >= 0.0 andalso B =< 255.0 -> ok; 30 | check_rgb(_) -> error. 31 | 32 | 33 | hsl2rgb(_Config) -> 34 | Degs = [0,1,10,20,30,50, 100, 127,128,150,175,200,220,240,250,255, 300,310,320,327,340,351,359.9,360], 35 | Fs = [ I/30 || I <- lists:seq(0, 30)], 36 | HSLs = [{H,S,L}||H <- Degs, S <- Fs, L <- Fs], 37 | 38 | ok = check_multiple_inputs(fun 39 | (HSL) -> check_rgb(egd_colorscheme:hsl2rgb(HSL)) 40 | end, HSLs), 41 | 42 | ok = check_multiple_inputs(fun 43 | ({{R,G,B} = RGB, HSL}) -> 44 | {R1, G1, B1, _} = RGB1 = egd_colorscheme:hsl2rgb(HSL), 45 | io:format("hsl2rgb(~p) -> ~p (<- expected) ~p (<- got)~n", [ 46 | HSL, RGB, RGB1]), 47 | true = check_value(R, R1), 48 | true = check_value(G, G1), 49 | true = check_value(B, B1), 50 | ok 51 | end, rgb_hsl_values()), 52 | ok. 53 | 54 | check_hsl({H, S, L, _}) -> check_hsl({H, S, L}); 55 | check_hsl({H, S, L}) when 56 | H >= 0.0 andalso H < 360.0 andalso 57 | S >= 0.0 andalso S =< 1.0 andalso 58 | L >= 0.0 andalso L =< 1.0 -> ok; 59 | check_hsl(_) -> error. 60 | 61 | rgb2hsl(_Config) -> 62 | Octs = [0,1,10,20,30,50, 100, 127,128,150,175,200,220,240,250,255], 63 | RGBs = [{R,G,B} || R<-Octs, G <- Octs, B <- Octs], 64 | 65 | ok = check_multiple_inputs(fun 66 | (RGB) -> check_hsl(egd_colorscheme:rgb2hsl(RGB)) 67 | end, RGBs), 68 | 69 | ok = check_multiple_inputs(fun 70 | ({RGB, {H, S, L} = HSL}) -> 71 | {H1, S1, L1, _} = HSL1 = egd_colorscheme:rgb2hsl(RGB), 72 | io:format("rgb2hsl(~p) -> ~p (<- expected) ~p (<- got)~n", [ 73 | RGB, HSL, HSL1]), 74 | true = check_value(H, H1), 75 | true = check_value(S, S1), 76 | true = check_value(L, L1), 77 | ok 78 | end, rgb_hsl_values()), 79 | 80 | ok. 81 | 82 | % aux 83 | 84 | rgb_hsl_values() -> 85 | [ 86 | {{255, 255, 255},{0, 0, 1}}, % white 87 | {{127, 127, 127},{0, 0, 0.5}}, % gray 88 | {{255, 0 , 0 },{0, 1, 0.5}}, % red 89 | {{127, 127, 255},{240, 1, 0.75}} 90 | ]. 91 | 92 | -define(float_error, 0.005). 93 | 94 | check_value(V1, V1) -> true; 95 | check_value(V1, V2) -> 96 | if 97 | abs(V1 - V2) < ?float_error -> true; 98 | true -> false 99 | end. 100 | 101 | check_multiple_inputs(_, []) -> ok; 102 | check_multiple_inputs(Fun, [I|Is]) -> 103 | case Fun(I) of 104 | ok -> check_multiple_inputs(Fun, Is); 105 | _ -> {failed, I} 106 | end. 107 | 108 | 109 | --------------------------------------------------------------------------------