├── .gitignore
├── build.zig
├── build.zig.zon
├── changelogs.md
├── example
├── area.zig
├── candle_stick.zig
├── line.zig
├── logarithmic.zig
├── out
│ ├── area.svg
│ ├── candlestick.svg
│ ├── line.svg
│ ├── logarithmic.svg
│ ├── scatter.svg
│ ├── stem.svg
│ └── step.svg
├── scatter.zig
├── stem.zig
└── step.zig
├── out.svg
├── readme.md
└── src
├── core
└── intf.zig
├── main.zig
├── plot
├── Area.zig
├── CandleStick.zig
├── Figure.zig
├── FigureInfo.zig
├── Line.zig
├── Marker.zig
├── Plot.zig
├── Scatter.zig
├── ShapeMarker.zig
├── Stem.zig
├── Step.zig
├── TextMarker.zig
└── formatters.zig
├── root.zig
├── svg
├── Circle.zig
├── Line.zig
├── Path.zig
├── Polyline.zig
├── Rect.zig
├── SVG.zig
├── Text.zig
├── kind.zig
└── util
│ ├── length.zig
│ └── rgb.zig
└── util
├── log.zig
├── polyshape.zig
├── range.zig
├── scale.zig
├── shape.zig
└── units.zig
/.gitignore:
--------------------------------------------------------------------------------
1 | /zig-cache
2 | /zig-out
3 | /.zig-cache
4 | /.vscode
5 | /doc
6 | Todo.md
--------------------------------------------------------------------------------
/build.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | fn addStartPoint(
4 | b: *std.Build,
5 | target: std.Build.ResolvedTarget,
6 | optimize: std.builtin.OptimizeMode,
7 | name: []const u8,
8 | description: []const u8,
9 | path: []const u8,
10 | module: *std.Build.Module,
11 | ) *std.Build.Step {
12 | const exe = b.addExecutable(.{
13 | .name = name,
14 | .root_source_file = b.path(path),
15 | .target = target,
16 | .optimize = optimize,
17 | });
18 |
19 | exe.root_module.addImport("zigplotlib", module);
20 |
21 | // This declares intent for the executable to be installed into the
22 | // standard location when the user invokes the "install" step (the default
23 | // step when running `zig build`).
24 | b.installArtifact(exe);
25 |
26 | // This *creates* a Run step in the build graph, to be executed when another
27 | // step is evaluated that depends on it. The next line below will establish
28 | // such a dependency.
29 | const run_cmd = b.addRunArtifact(exe);
30 |
31 | // By making the run step depend on the install step, it will be run from the
32 | // installation directory rather than directly from within the cache directory.
33 | // This is not necessary, however, if the application depends on other installed
34 | // files, this ensures they will be present and in the expected location.
35 | run_cmd.step.dependOn(b.getInstallStep());
36 |
37 | // This allows the user to pass arguments to the application in the build
38 | // command itself, like this: `zig build run -- arg1 arg2 etc`
39 | if (b.args) |args| {
40 | run_cmd.addArgs(args);
41 | }
42 |
43 | // This creates a build step. It will be visible in the `zig build --help` menu,
44 | // and can be selected like this: `zig build run`
45 | // This will evaluate the `run` step rather than the default, which is "install".
46 | const run_step = b.step(name, description);
47 | run_step.dependOn(&run_cmd.step);
48 |
49 | return run_step;
50 | }
51 |
52 | // Although this function looks imperative, note that its job is to
53 | // declaratively construct a build graph that will be executed by an external
54 | // runner.
55 | pub fn build(b: *std.Build) void {
56 | // Standard target options allows the person running `zig build` to choose
57 | // what target to build for. Here we do not override the defaults, which
58 | // means any target is allowed, and the default is native. Other options
59 | // for restricting supported target set are available.
60 | const target = b.standardTargetOptions(.{});
61 |
62 | // Standard optimization options allow the person running `zig build` to select
63 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
64 | // set a preferred release mode, allowing the user to decide how to optimize.
65 | const optimize = b.standardOptimizeOption(.{});
66 |
67 | const lib = b.addStaticLibrary(.{
68 | .name = "zigplotlib",
69 | // In this case the main source file is merely a path, however, in more
70 | // complicated build scripts, this could be a generated file.
71 | .root_source_file = b.path("src/root.zig"),
72 | .target = target,
73 | .optimize = optimize,
74 | });
75 |
76 | const lib_module = &lib.root_module;
77 |
78 | _ = b.addModule("zigplotlib", .{
79 | .root_source_file = b.path("src/root.zig"),
80 | });
81 |
82 | // This declares intent for the library to be installed into the standard
83 | // location when the user invokes the "install" step (the default step when
84 | // running `zig build`).
85 | b.installArtifact(lib);
86 |
87 | const run_step = addStartPoint(b, target, optimize, "run", "Run the App", "src/main.zig", lib_module);
88 | const step_step = addStartPoint(b, target, optimize, "step-example", "Run the Step example", "example/step.zig", lib_module);
89 | const stem_step = addStartPoint(b, target, optimize, "stem-example", "Run the Stem example", "example/stem.zig", lib_module);
90 | const scatter_step = addStartPoint(b, target, optimize, "scatter-example", "Run the Scatter example", "example/scatter.zig", lib_module);
91 | const line_step = addStartPoint(b, target, optimize, "line-example", "Run the Line example", "example/line.zig", lib_module);
92 | const area_step = addStartPoint(b, target, optimize, "area-example", "Run the Area example", "example/area.zig", lib_module);
93 | const log_step = addStartPoint(b, target, optimize, "log-example", "Run the Logarithmic example", "example/logarithmic.zig", lib_module);
94 | const candlestick_step = addStartPoint(b, target, optimize, "candlestick-example", "Run the Candle stick example", "example/candle_stick.zig", lib_module);
95 |
96 | const all_step = b.step("all", "Run all the examples");
97 | all_step.dependOn(run_step);
98 | all_step.dependOn(step_step);
99 | all_step.dependOn(stem_step);
100 | all_step.dependOn(scatter_step);
101 | all_step.dependOn(line_step);
102 | all_step.dependOn(area_step);
103 | all_step.dependOn(log_step);
104 | all_step.dependOn(candlestick_step);
105 |
106 | // Creates a step for unit testing. This only builds the test executable
107 | // but does not run it.
108 | const lib_unit_tests = b.addTest(.{
109 | .root_source_file = b.path("src/root.zig"),
110 | .target = target,
111 | .optimize = optimize,
112 | });
113 |
114 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
115 |
116 | const exe_unit_tests = b.addTest(.{
117 | .root_source_file = b.path("src/main.zig"),
118 | .target = target,
119 | .optimize = optimize,
120 | });
121 |
122 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
123 |
124 | // Similar to creating the run step earlier, this exposes a `test` step to
125 | // the `zig build --help` menu, providing a way for the user to request
126 | // running the unit tests.
127 | const test_step = b.step("test", "Run unit tests");
128 | test_step.dependOn(&run_lib_unit_tests.step);
129 | test_step.dependOn(&run_exe_unit_tests.step);
130 | }
131 |
--------------------------------------------------------------------------------
/build.zig.zon:
--------------------------------------------------------------------------------
1 | .{
2 | .name = "zigplotlib",
3 | .version = "0.1.4",
4 | .minimum_zig_version = "0.13.0",
5 | .dependencies = .{},
6 |
7 | .paths = .{
8 | "",
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/changelogs.md:
--------------------------------------------------------------------------------
1 | # Version 0.1.4
2 | - Added Markers to the figure
3 | - Added the ShapeMarker & TextMarker
4 |
5 | # Version 0.1.3
6 | - Added the CandleStick plot type (see [example](example/candle_stick.zig))
7 | - Added the `show_x_labels` and `show_y_labels` options to figure
8 | - Added the `x_labels_formatter` and `y_labels_formatter` options to figure
9 | - Added the `default` and `scientific` formatters for the labels
10 |
11 | # Version 0.1.2
12 | - Added smooth option to line plot
13 |
14 | # Version 0.1.1
15 | - Added the logarithmic example
16 | - Added the logarithmic scale
17 | - Added the possibility to add a title to the figure.
18 | - Updated the `build.zig` for Zig 0.12.0 dev.3552+b88ae8dbd (@lcscosta)
--------------------------------------------------------------------------------
/example/area.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const rgb = zigplotlib.rgb;
7 | const Range = zigplotlib.Range;
8 |
9 | const Figure = zigplotlib.Figure;
10 | const Area = zigplotlib.Area;
11 |
12 | pub fn main() !void {
13 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14 | defer _ = gpa.deinit();
15 | const allocator = gpa.allocator();
16 |
17 | var x: [28]f32 = undefined;
18 | var y: [28]f32 = undefined;
19 | var y2: [28]f32 = undefined;
20 | for (0..28) |i| {
21 | x[i] = @floatFromInt(i);
22 | y[i] = std.math.sin(x[i] / 4.0);
23 | y2[i] = std.math.sin(x[i] / 4.0) + 1;
24 | }
25 |
26 | var figure = Figure.init(allocator, .{
27 | .axis = .{
28 | .show_y_axis = false,
29 | }
30 | });
31 | defer figure.deinit();
32 | try figure.addPlot(Area {
33 | .x = &x,
34 | .y = &y2,
35 | .style = .{
36 | .color = rgb.GRAY,
37 | .width = 2.0,
38 | }
39 | });
40 | try figure.addPlot(Area {
41 | .x = &x,
42 | .y = &y,
43 | .style = .{
44 | .color = rgb.BLUE,
45 | .width = 2.0,
46 | }
47 | });
48 | var svg = try figure.show();
49 | defer svg.deinit();
50 |
51 | // Write to an output file (out.svg)
52 | var file = try std.fs.cwd().createFile("example/out/area.svg", .{});
53 | defer file.close();
54 |
55 | try svg.writeTo(file.writer());
56 | }
--------------------------------------------------------------------------------
/example/candle_stick.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const Figure = zigplotlib.Figure;
7 | const CandleStick = zigplotlib.CandleStick;
8 |
9 | pub fn main() !void {
10 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
11 | defer _ = gpa.deinit();
12 | const allocator = gpa.allocator();
13 |
14 | var candles: [10]CandleStick.Candle = .{
15 | CandleStick.Candle{
16 | .open = 1.0,
17 | .close = 1.2,
18 | .low = 0.9,
19 | .high = 1.5,
20 | },
21 | CandleStick.Candle{
22 | .open = 1.2,
23 | .close = 1.3,
24 | .low = 1.1,
25 | .high = 1.4,
26 | },
27 | CandleStick.Candle{
28 | .open = 1.3,
29 | .close = 1.1,
30 | .low = 1.0,
31 | .high = 1.4,
32 | },
33 | CandleStick.Candle{
34 | .open = 1.1,
35 | .close = 1.4,
36 | .low = 1.0,
37 | .high = 1.5,
38 | },
39 | CandleStick.Candle{
40 | .open = 1.4,
41 | .close = 2.4,
42 | .low = 1.3,
43 | .high = 3.1,
44 | },
45 | CandleStick.Candle{
46 | .open = 2.4,
47 | .close = 2.6,
48 | .low = 2.3,
49 | .high = 2.7,
50 | },
51 | CandleStick.Candle{
52 | .open = 2.6,
53 | .close = 2.2,
54 | .low = 1.8,
55 | .high = 2.7,
56 | },
57 | CandleStick.Candle{
58 | .open = 2.2,
59 | .close = 1.6,
60 | .low = 1.5,
61 | .high = 2.3,
62 | .color = 0x6688FF,
63 | },
64 | CandleStick.Candle{
65 | .open = 1.6,
66 | .close = 1.8,
67 | .low = 1.5,
68 | .high = 1.9,
69 | },
70 | CandleStick.Candle{
71 | .open = 1.8,
72 | .close = 1.9,
73 | .low = 1.7,
74 | .high = 2.0,
75 | },
76 | };
77 |
78 | var figure = Figure.init(allocator, .{
79 | .title = .{
80 | .text = "CandleStick example",
81 | },
82 | .axis = .{
83 | .show_grid_x = false,
84 | .show_grid_y = false,
85 | .show_x_labels = false,
86 | .y_labels_formatter = Figure.formatters.default(.{}),
87 | },
88 | });
89 | defer figure.deinit();
90 |
91 | try figure.addPlot(CandleStick{
92 | .candles = &candles,
93 | .style = .{},
94 | });
95 |
96 | var svg = try figure.show();
97 | defer svg.deinit();
98 |
99 | // Write to an output file (out.svg)
100 | var file = try std.fs.cwd().createFile("example/out/candlestick.svg", .{});
101 | defer file.close();
102 |
103 | try svg.writeTo(file.writer());
104 | }
105 |
--------------------------------------------------------------------------------
/example/line.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const rgb = zigplotlib.rgb;
7 | const Range = zigplotlib.Range;
8 |
9 | const Figure = zigplotlib.Figure;
10 | const Line = zigplotlib.Line;
11 | const ShapeMarker = zigplotlib.ShapeMarker;
12 |
13 | const SMOOTHING = 0.2;
14 |
15 | pub fn main() !void {
16 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
17 | defer _ = gpa.deinit();
18 | const allocator = gpa.allocator();
19 |
20 | var x: [28]f32 = undefined;
21 | var y: [28]f32 = undefined;
22 | var y2: [28]f32 = undefined;
23 | for (0..28) |i| {
24 | x[i] = @floatFromInt(i);
25 | y[i] = std.math.sin(x[i] / 4.0);
26 | y2[i] = std.math.sin(x[i] / 4.0) + 1;
27 | }
28 |
29 | var figure = Figure.init(allocator, .{
30 | .value_padding = .{
31 | .x_min = .{ .value = 1.0 },
32 | .x_max = .{ .value = 1.0 },
33 | },
34 | .axis = .{
35 | .show_y_axis = false,
36 | },
37 | });
38 | defer figure.deinit();
39 | try figure.addPlot(Line{ .x = &x, .y = &y, .style = .{
40 | .color = rgb.BLUE,
41 | .width = 2.0,
42 | .smooth = SMOOTHING,
43 | } });
44 | try figure.addPlot(Line{ .x = &x, .y = &y2, .style = .{
45 | .color = rgb.GRAY,
46 | .width = 2.0,
47 | .dash = 4.0,
48 | .smooth = SMOOTHING,
49 | } });
50 |
51 | try figure.addMarker(ShapeMarker{
52 | .x = 9.33,
53 | .y = 0.73,
54 | .shape = .cross,
55 | .color = 0xFF0000,
56 | .size = 6.0,
57 | });
58 | try figure.addMarker(ShapeMarker{
59 | .x = 18.67,
60 | .y = 0,
61 | .shape = .circle_outline,
62 | .color = 0x00FF00,
63 | .size = 8.0,
64 | .label = "Bottom",
65 | .label_weight = .w600,
66 | });
67 |
68 | var svg = try figure.show();
69 | defer svg.deinit();
70 |
71 | // Write to an output file (out.svg)
72 | var file = try std.fs.cwd().createFile("example/out/line.svg", .{});
73 | defer file.close();
74 |
75 | try svg.writeTo(file.writer());
76 | }
77 |
--------------------------------------------------------------------------------
/example/logarithmic.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const rgb = zigplotlib.rgb;
7 | const Range = zigplotlib.Range;
8 |
9 | const Figure = zigplotlib.Figure;
10 | const Line = zigplotlib.Line;
11 |
12 | pub fn main() !void {
13 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14 | defer _ = gpa.deinit();
15 | const allocator = gpa.allocator();
16 |
17 | var x: [221]f32 = undefined;
18 | var y: [221]f32 = undefined;
19 | var y2: [221]f32 = undefined;
20 | var y3: [221]f32 = undefined;
21 | for (0..221) |i| {
22 | x[i] = 0.05 * @as(f32, @floatFromInt(@as(i32, @intCast(i)) - 20));
23 | y[i] = std.math.pow(f32, 10, x[i]);
24 | y2[i] = x[i];
25 | y3[i] = std.math.log10(x[i]);
26 | }
27 |
28 | // Used to snap to the grid (will be fixed in later updates).
29 | y3[44] = 0.10;
30 |
31 | var figure = Figure.init(allocator, .{
32 | .axis = .{
33 | .y_scale = .log,
34 | .x_range = Range(f32){ .min = -1.0, .max = 10.0 },
35 | .tick_count_y = .{ .count = 4 },
36 | .y_range = Range(f32){ .min = 0.1, .max = 1000.0 },
37 | },
38 | });
39 |
40 | defer figure.deinit();
41 | try figure.addPlot(Line{
42 | .x = &x,
43 | .y = &y,
44 | .style = .{
45 | .color = rgb.RED,
46 | .width = 2.0,
47 | .smooth = 0.2,
48 | },
49 | });
50 |
51 | try figure.addPlot(Line{
52 | .x = &x,
53 | .y = &y2,
54 | .style = .{
55 | .color = rgb.GREEN,
56 | .width = 2.0,
57 | .smooth = 0.2,
58 | },
59 | });
60 |
61 | try figure.addPlot(Line{
62 | .x = &x,
63 | .y = &y3,
64 | .style = .{ .color = rgb.BLUE, .width = 2.0, .smooth = 0.2 },
65 | });
66 |
67 | var svg = try figure.show();
68 | defer svg.deinit();
69 |
70 | // Write to an output file (out.svg)
71 | var file = try std.fs.cwd().createFile("example/out/logarithmic.svg", .{});
72 | defer file.close();
73 |
74 | try svg.writeTo(file.writer());
75 | }
76 |
--------------------------------------------------------------------------------
/example/out/area.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/out/candlestick.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/out/line.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/out/scatter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/out/stem.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/out/step.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/scatter.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const rgb = zigplotlib.rgb;
7 | const Range = zigplotlib.Range;
8 |
9 | const Figure = zigplotlib.Figure;
10 | const Scatter = zigplotlib.Scatter;
11 |
12 | pub fn main() !void {
13 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14 | defer _ = gpa.deinit();
15 | const allocator = gpa.allocator();
16 |
17 | var xoshiro = std.rand.Xoshiro256.init(100);
18 | var rand = xoshiro.random();
19 |
20 | var x: [28]f32 = undefined;
21 | var y1: [28]f32 = undefined;
22 | var y2: [28]f32 = undefined;
23 | for (0..28) |i| {
24 | x[i] = @floatFromInt(i);
25 | const r = rand.float(f32);
26 | y1[i] = x[i] + r * 10.0 - 5.0;
27 | y2[i] = x[i] + r * 2.0 - 1.0;
28 | }
29 |
30 | var figure = Figure.init(allocator, .{
31 | .value_padding = .{
32 | .x_min = .{ .value = 1.0 },
33 | .x_max = .{ .value = 1.0 },
34 | },
35 | .axis = .{
36 | .show_y_axis = false,
37 | }
38 | });
39 | defer figure.deinit();
40 |
41 | try figure.addPlot(Scatter {
42 | .x = &x,
43 | .y = &y1,
44 | .style = .{
45 | .color = rgb.BLUE,
46 | .radius = 4.0,
47 | .shape = .circle
48 | }
49 | });
50 | try figure.addPlot(Scatter {
51 | .x = &x,
52 | .y = &y2,
53 | .style = .{
54 | .color = rgb.RED,
55 | .radius = 4.0,
56 | .shape = .rhombus
57 | }
58 | });
59 |
60 | var svg = try figure.show();
61 | defer svg.deinit();
62 |
63 | // Write to an output file (out.svg)
64 | var file = try std.fs.cwd().createFile("example/out/scatter.svg", .{});
65 | defer file.close();
66 |
67 | try svg.writeTo(file.writer());
68 | }
--------------------------------------------------------------------------------
/example/stem.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const rgb = zigplotlib.rgb;
7 | const Range = zigplotlib.Range;
8 |
9 | const Figure = zigplotlib.Figure;
10 | const Stem = zigplotlib.Stem;
11 |
12 | pub fn main() !void {
13 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14 | defer _ = gpa.deinit();
15 | const allocator = gpa.allocator();
16 |
17 | var x: [14]f32 = undefined;
18 | var y: [14]f32 = undefined;
19 | for (0..14) |i| {
20 | x[i] = @floatFromInt(i);
21 | y[i] = std.math.sin(x[i] / 2.0);
22 | }
23 |
24 | var figure = Figure.init(allocator, .{
25 | .value_padding = .{
26 | .x_min = .{ .value = 1.0 },
27 | .x_max = .{ .value = 1.0 },
28 | },
29 | .axis = .{
30 | .show_y_axis = false,
31 | }
32 | });
33 | defer figure.deinit();
34 |
35 | try figure.addPlot(Stem {
36 | .x = &x,
37 | .y = &y,
38 | .style = .{
39 | .color = rgb.BLUE,
40 | .width = 4.0,
41 | }
42 | });
43 | var svg = try figure.show();
44 | defer svg.deinit();
45 |
46 | // Write to an output file (out.svg)
47 | var file = try std.fs.cwd().createFile("example/out/stem.svg", .{});
48 | defer file.close();
49 |
50 | try svg.writeTo(file.writer());
51 | }
--------------------------------------------------------------------------------
/example/step.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const zigplotlib = @import("zigplotlib");
4 | const SVG = zigplotlib.SVG;
5 |
6 | const rgb = zigplotlib.rgb;
7 | const Range = zigplotlib.Range;
8 |
9 | const Figure = zigplotlib.Figure;
10 | const Line = zigplotlib.Line;
11 | const Scatter = zigplotlib.Scatter;
12 | const Step = zigplotlib.Step;
13 |
14 | pub fn main() !void {
15 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
16 | defer _ = gpa.deinit();
17 | const allocator = gpa.allocator();
18 |
19 | var x: [14]f32 = undefined;
20 | var y: [14]f32 = undefined;
21 | for (0..14) |i| {
22 | x[i] = @floatFromInt(i);
23 | y[i] = std.math.sin(x[i] / 2.0);
24 | }
25 |
26 | var figure = Figure.init(allocator, .{
27 | .value_padding = .{
28 | .x_min = .{ .value = 1.0 },
29 | .x_max = .{ .value = 1.0 },
30 | },
31 | .axis = .{
32 | .show_y_axis = false,
33 | }
34 | });
35 | defer figure.deinit();
36 |
37 | try figure.addPlot(Scatter {
38 | .x = &x,
39 | .y = &y,
40 | .style = .{
41 | .color = rgb.GRAY,
42 | .radius = 4.0,
43 | .shape = .circle,
44 | }
45 | });
46 | try figure.addPlot(Line {
47 | .x = &x,
48 | .y = &y,
49 | .style = .{
50 | .color = rgb.GRAY,
51 | .width = 2.0,
52 | .dash = 4.0,
53 | }
54 | });
55 | try figure.addPlot(Step {
56 | .x = &x,
57 | .y = &y,
58 | .style = .{
59 | .color = 0x0000FF,
60 | }
61 | });
62 | var svg = try figure.show();
63 | defer svg.deinit();
64 |
65 | // Write to an output file (out.svg)
66 | var file = try std.fs.cwd().createFile("example/out/step.svg", .{});
67 | defer file.close();
68 |
69 | try svg.writeTo(file.writer());
70 | }
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Zig Plot Lib
2 | > This project is currently stalled as I don't have much time to work on it. Anybody can freely create a PR to add new features and I'll review it; or you can also fork it yourself and add whatever you would like.
3 |
4 | The Zig Plot Lib is a library for plotting data in Zig. It is designed to be easy to use and to have a simple API.
5 |
6 | **Note:** This library is still in development and is not yet ready for production use.
7 |
8 | I'm developping this library with version 0.13.0.
9 |
10 | ## Installation
11 | You can install the library by adding it to the `build.zig.zon` file, either manually like so:
12 | ```zig
13 | .{
14 | ...
15 | .dependencies = .{
16 | .zigplotlib = .{
17 | .url = "https://github.com/Remy2701/zigplotlib/archive/main.tar.gz",
18 | .hash = "...",
19 | }
20 | }
21 | ...
22 | }
23 | ```
24 |
25 | The hash can be found using the builtin command:
26 | ```sh
27 | zig fetch https://github.com/Remy2701/zigplotlib/archive/main.tar.gz
28 | ```
29 |
30 | Or you can also add it automatically like so:
31 | ```sh
32 | zig fetch --save https://github.com/Remy2701/zigplotlib/archive/main.tar.gz
33 | ```
34 |
35 | Then in the `build.zig`, you can add the following:
36 | ```zig
37 | const zigplotlib = b.dependency("zigplotlib", .{
38 | .target = target,
39 | .optimize = optimize,
40 | });
41 |
42 | exe.root_module.addImport("plotlib", zigplotlib.module("zigplotlib"));
43 | ```
44 |
45 | The name of the module (`plotlib`) can be changed to whatever you want.
46 |
47 | Finally in your code you can import the module using the following:
48 | ```zig
49 | const plotlib = @import("plotlib");
50 | ```
51 |
52 | ## Example
53 |
54 | 
55 |
56 | The above plot was generated with the following code:
57 |
58 | ```zig
59 | const std = @import("std");
60 |
61 | const SVG = @import("svg/SVG.zig");
62 |
63 | const Figure = @import("plot/Figure.zig");
64 | const Line = @import("plot/Line.zig");
65 | const Area = @import("plot/Area.zig");
66 | const Scatter = @import("plot/Scatter.zig");
67 |
68 | /// The function for the 1st plot (area - blue)
69 | fn f(x: f32) f32 {
70 | if (x > 10.0) {
71 | return 20 - (2 * (x - 10.0));
72 | }
73 | return 2 * x;
74 | }
75 |
76 | /// The function for the 2nd plot (scatter - red)
77 | fn f2(x: f32) f32 {
78 | if (x > 10.0) {
79 | return 10.0;
80 | }
81 | return x;
82 | }
83 |
84 | /// The function for the 3rd plot (line - green)
85 | fn f3(x: f32) f32 {
86 | if (x < 8.0) {
87 | return 0.0;
88 | }
89 | return 0.5 * (x - 8.0);
90 | }
91 |
92 | pub fn main() !void {
93 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
94 | defer _ = gpa.deinit();
95 | const allocator = gpa.allocator();
96 |
97 | var points: [25]f32 = undefined;
98 | var points2: [25]f32 = undefined;
99 | var points3: [25]f32 = undefined;
100 | for (0..25) |i| {
101 | points[i] = f(@floatFromInt(i));
102 | points2[i] = f2(@floatFromInt(i));
103 | points3[i] = f3(@floatFromInt(i));
104 | }
105 |
106 | var figure = Figure.init(allocator, .{
107 | .title = .{
108 | .text = "Example plot",
109 | },
110 | });
111 | defer figure.deinit();
112 |
113 | try figure.addPlot(Area {
114 | .y = &points,
115 | .style = .{
116 | .color = 0x0000FF,
117 | }
118 | });
119 | try figure.addPlot(Scatter {
120 | .y = &points2,
121 | .style = .{
122 | .shape = .plus,
123 | .color = 0xFF0000,
124 | }
125 | });
126 | try figure.addPlot(Line {
127 | .y = &points3,
128 | .style = .{
129 | .color = 0x00FF00,
130 | }
131 | });
132 | try figure.addPlot(Area {
133 | .x = &[_]f32 { -5.0, 0.0, 5.0 },
134 | .y = &[_]f32 { 5.0, 3.0, 5.0 },
135 | .style = .{
136 | .color = 0xFF00FF,
137 | }
138 | });
139 | try figure.addPlot(Area {
140 | .x = &[_]f32 { -5.0, 0.0, 5.0 },
141 | .y = &[_]f32 { -5.0, -3.0, -5.0 },
142 | .style = .{
143 | .color = 0xFFFF00,
144 | }
145 | });
146 |
147 | var svg = try figure.show();
148 | defer svg.deinit();
149 |
150 | // Write to an output file (out.svg)
151 | var file = try std.fs.cwd().createFile("out.svg", .{});
152 | defer file.close();
153 |
154 | try svg.writeTo(file.writer());
155 | }
156 | ```
157 |
158 | ## Usage
159 |
160 | The first thing needed is to create a figure which will contain the plots.
161 |
162 | ```zig
163 | const std = @import("std");
164 | const Figure = @import("plot/Figure.zig");
165 |
166 | pub fn main() !void {
167 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
168 | defer _ = gpa.deinit();
169 | const allocator = gpa.allocator();
170 |
171 | var figure = Figure.init(allocator, .{});
172 | defer figure.deinit();
173 | }
174 | ```
175 |
176 | The figure takes two arguments, the allocator (used to store the plot and generate the SVG) and the style for the plot. The options available for the style are:
177 |
178 | | Option | Type | Description |
179 | | --- | --- | --- |
180 | | `width` | `union(enum) { pixel: f32, auto_gap: f32 }` | The width of the plot in pixels (excluding the axis and label). |
181 | | `height` | `union(enum) { pixel: f32, auto_gap: f32 }` | The height of the plot in pixels (excluding the axis and label). |
182 | | `plot_padding` | `f32` | The padding around the plot |
183 | | `background_color` | `RGB (u48)` | The background color of the plot |
184 | | `background_opacity` | `f32` | The opacity of the background |
185 | | `title` | `?...` | The style of the title (null to hide it) |
186 | | `value_padding` | `...` | The padding to use for the range of the plot |
187 | | `axis` | `...` | The style for the axis |
188 | | `legend` | `...` | The style for the legend |
189 |
190 | The `title` option contains the following parameters:
191 | | Option | Type | Description |
192 | | --- | --- | --- |
193 | | `text` | `[]const u8` | The title of the figure |
194 | | `position` | `enum { top, bottom }` | The position of the title |
195 | | `font_size` | `f32` | The font size of the title |
196 | | `color` | `RGB (u48)` | The color of the title |
197 | | `padding` | `f32` | The padding between the plot and the title |
198 |
199 | The `value_padding` option is defined like so:
200 | ```zig
201 | pub const ValuePercent = union(enum) {
202 | value: f32,
203 | percent: f32,
204 | };
205 |
206 | value_padding: struct {
207 | x_max: ValuePercent,
208 | y_max: ValuePercent,
209 | x_min: ValuePercent,
210 | y_min: ValuePercent,
211 | },
212 | ```
213 |
214 | The `axis` option contains more parameters:
215 |
216 | | Option | Type | Description |
217 | | --- | --- | --- |
218 | | `x_scale` | `enum { linear, log }` | The scale of the x axis |
219 | | `y_scale` | `enum { linear, log }` | The scale of the y axis |
220 | | `x_range` | `?Range(f32)` | The range of values for the x axis |
221 | | `y_range` | `?Range(f32)` | The range of values for the y axis |
222 | | `color` | `RGB (u48)` | The color of the axis |
223 | | `width` | `f32` | The width of the axis |
224 | | `label_color` | `RGB (u48)` | The color of the labels |
225 | | `label_size` | `f32` | The font size of the labels |
226 | | `label_padding` | `f32` | The padding between the labels and the axis |
227 | | `label_font` | `[]const u8` | The font to use for the labels |
228 | | `tick_count_x` | `...` | The number of ticks to use on the x axis |
229 | | `tick_count_y` | `...` | The number of ticks to use on the y axis |
230 | | `show_x_axis` | `bool` | whether to show the x axis |
231 | | `show_y_axis` | `bool` | whether to show the y axis |
232 | | `show_grid_x` | `bool` | whether to show the grid on the x axis |
233 | | `show_grid_y` | `bool` | whether to show the grid on the y axis |
234 | | `grid_opacity` | `f32` | The opacity of the grid |
235 | | `frame_color` | `RGB (u48)` | The color of the frame |
236 | | `frame_width` | `f32` | The width of the frame |
237 |
238 | The `tick_count_x` and `tick_count_y` options are defined like so:
239 | ```zig
240 | tick_count_x: union(enum) {
241 | count: usize,
242 | gap: f32,
243 | }
244 | ```
245 |
246 | The `legend` option contains more parameters:
247 |
248 | | Option | Type | Description |
249 | | --- | --- | --- |
250 | | `show` | `bool` | Whether to show the legend |
251 | | `position` | `enum { top_left, top_right, bottom_left, bottom_right }` | The position of the legend |
252 | | `font_size` | `f32` | The font size of the legend |
253 | | `background_color` | `RGB (u48)` | The background color of the legend |
254 | | `border_color` | `RGB (u48)` | The border color of the legend |
255 | | `border_width` | `f32` | The border width of the legend |
256 | | `padding` | `f32` | The padding around the legend |
257 |
258 | Then you can add a plot like so (here is the example with the line plot):
259 |
260 | ```zig
261 | const Line = @import("plot/Line.zig");
262 | ...
263 | figure.addPlot(Line {
264 | .y = points,
265 | .style = .{
266 | .color = 0x0000FF,
267 | }
268 | });
269 | ```
270 |
271 | ## Supported Plots
272 |
273 | There are currently 6 types of plots supported:
274 |
275 | ### Line
276 |
277 | 
278 |
279 | The options for styling the line plot are:
280 |
281 | | Option | Type | Description |
282 | | --- | --- | --- |
283 | | `title` | `?[]const u8` | The title of the plot (used for the legend) |
284 | | `color` | `RGB (u48)` | The color of the line |
285 | | `width` | `f32` | The width of the line |
286 | | `dash` | `?f32` | The length of the dash for the line (null means no dash) |
287 | | `smooth` | `f32` | The smoothing factor for the line plot. It must be in range [0; 1]. (0 means no smoothing). |
288 |
289 | ### Area
290 |
291 | 
292 |
293 | The options for styling the area plot are:
294 |
295 | | Option | Type | Description |
296 | | --- | --- | --- |
297 | | `title` | `?[]const u8` | The title of the plot (used for the legend) |
298 | | `color` | `RGB (u48)` | The color of the area |
299 | | `opacity` | `f32` | The opacity of the area |
300 | | `width` | `f32` | The width of the line (above the area) |
301 |
302 | ### Scatter
303 |
304 | 
305 |
306 | The options for styling the scatter plot are:
307 |
308 | | Option | Type | Description |
309 | | --- | --- | --- |
310 | | `title` | `?[]const u8` | The title of the plot (used for the legend) |
311 | | `color` | `RGB (u48)` | The color of the points |
312 | | `radius` | `f32` | The radius of the points |
313 | | `shape` | `...` | The shape of the points |
314 |
315 | The available shapes are:
316 |
317 | | Shape | Description |
318 | | --- | --- |
319 | | `circle` | A circle |
320 | | `circle_outline` | The outline of a circle |
321 | | `square` | A square |
322 | | `square_outline` | The outline of a square |
323 | | `triangle` | A triangle (facing upwards) |
324 | | `triangle_outline` | The outline of a triangle (facing upwards) |
325 | | `rhombus` | A rhombus |
326 | | `rhombus_outline` | The outline of a rhombus |
327 | | `plus` | A plus sign |
328 | | `plus_outline` | The outline of a plus sign |
329 | | `cross` | A cross |
330 | | `cross_outline` | The outline of a cross |
331 |
332 | ### Step
333 |
334 | 
335 |
336 | The first value of the x and y arrays are used as the starting point of the plot, this means that the step will start from this point. The options for styling the step plot are:
337 |
338 | | Option | Type | Description |
339 | | --- | --- | --- |
340 | | `title` | `?[]const u8` | The title of the plot (used for the legend) |
341 | | `color` | `RGB (u48)` | The color of the line |
342 | | `width` | `f32` | The width of the line |
343 |
344 | ### Stem
345 |
346 | 
347 |
348 | The options for styling the stem plot are:
349 |
350 | | Option | Type | Description |
351 | | --- | --- | --- |
352 | | `title` | `?[]const u8` | The title of the plot (used for the legend) |
353 | | `color` | `RGB (u48)` | The color of the stem |
354 | | `width` | `f32` | The width of the stem |
355 | | `shape` | `Shape` | The shape of the points (at the end of the stem) |
356 | | `radius` | `f32` | The radius of the points (at the end of the stem) |
357 |
358 | ### Candlestick
359 |
360 | 
361 |
362 | The options for styling the candlestick plot are:
363 |
364 | | Option | Type | Description |
365 | | --- | --- | --- |
366 | | `title` | `?[]const u8` | The title of the plot (used for the legend) |
367 | | `inc_color` | `RGB (u48)` | The color of the increasing candlestick |
368 | | `dec_color` | `RGB (u48)` | The color of the decreasing candlestick |
369 | | `width` | `f32` | The width of the candle |
370 | | `gap` | `f32` | The gap between the candles |
371 | | `line_thickness` | `f32` | The thickness of the sticks |
372 |
373 | The CandleStick plot works a bit differently, it doesn't take a `[]f32` but a `[]Candle`, named `candles` (there is no value for the x-axis).
374 |
375 | The parameters for the candle are as follows:
376 |
377 | | Field | Type | Description |
378 | | --- | --- | --- |
379 | | `open` | `f32` | The opening price of the candle |
380 | | `close` | `f32` | The closing price of the candle |
381 | | `high` | `f32` | The highest price of the candle |
382 | | `low` | `f32` | The lowest price of the candle |
383 | | `color` | `?RGB (u48)` | The color of the candle (overrides the default one) |
384 |
385 | ## Supported Markers
386 | You can add a marker to the plot using the `addMarker` function.
387 | There are currently 2 types of markers supported:
388 |
389 | ### ShapeMarker
390 | The shape marker allows you to write the plot with a shape.
391 |
392 | The options for the shape marker are:
393 |
394 | | Option | Type | Description |
395 | | --- | --- | --- |
396 | | `x` | `f32` | The x coordinate of the marker |
397 | | `y` | `f32` | The y coordinate of the marker |
398 | | `shape` | `Shape` | The shape of the marker |
399 | | `size` | `f32` | The size of the marker |
400 | | `color` | `RGB (u48)` | The color of the marker |
401 | | `label` | `?[]const u8` | The label of the marker |
402 | | `label_color` | `?RGB (u48)` | The color of the label (default to the same as the shape) |
403 | | `label_size` | `f32` | The size of the label |
404 | | `label_weight` | `FontWeight` | The weight of the label |
405 |
406 |
407 | ### TextMarker
408 | The Text marker is similar to the shape marker, but there is no shape, only text.
409 |
410 | The options for the text marker are:
411 |
412 | | Option | Type | Description |
413 | | --- | --- | --- |
414 | | `x` | `f32` | The x coordinate of the marker |
415 | | `y` | `f32` | The y coordinate of the marker |
416 | | `size` | `f32` | The size of the text |
417 | | `color` | `RGB (u48)` | The color of the text |
418 | | `text` | `[]const u8` | The text of the marker |
419 | | `weight` | `FontWeight` | The weight of the text |
420 |
421 | ## Create a new plot type
422 | In order to create a new type of plot, all that is needed is to create a struct that contains an `interface` function, defined as follows:
423 |
424 | ```zig
425 | pub fn interface(self: *const Self) Plot {
426 | ...
427 | }
428 | ```
429 |
430 | The `Plot` object, contains the following fields:
431 | - a pointer to the data (`*const anyopaque`)
432 | - the title of the plot (`?[]const u8`) (used for the legend)
433 | - the color of the plot (`RGB (u48)`) (used for the legend)
434 | - a pointer to the get_range_x function `*const fn(*const anyopaque) Range(f32)`
435 | - a pointer to the get_range_y function `*const fn(*const anyopaque) Range(f32)`
436 | - a pointer to the draw function `*const fn(*const anyopaque, Allocator, *SVG, FigureInfo) anyerror!void`
437 |
438 | You can look at the implementation of the `Line`, `Scatter`, `Area`, `Step`, `Stem`, or `CandleStick` plots for examples.
439 |
440 | ## Create a new marker type
441 | Same as for the plots, to create a new type of marker, all that is needed is to create a struct that contains an `interface` function, defined as follows:
442 |
443 | ```zig
444 | pub fn interface(self: *const Self) Marker {
445 | ...
446 | }
447 | ```
448 |
449 | The `Marker` object, contains the following fields:
450 | - a pointer to the data (`*const anyopaque`)
451 | - a pointer to the draw function `*const fn(*const anyopaque, Allocator, *SVG, FigureInfo) anyerror!void`
452 |
453 | You can look at the implementation of the `ShapeMarker` or `TextMarker` for examples.
454 |
455 | ## Roadmap
456 | - Ability to set the title of the axis
457 | - Ability to add arrows at the end of axis
458 | - More plot types
459 | - Bar
460 | - Histogram
461 | - Linear Interpolation with the figure border
462 | - Themes
463 |
464 | ### Known issue(s)
465 | - Imperfect text width calculation for the legend (only when the legend is positioned on the right)
466 |
--------------------------------------------------------------------------------
/src/core/intf.zig:
--------------------------------------------------------------------------------
1 | //! Utility Interface Functions
2 |
3 | const std = @import("std");
4 |
5 | const comptimePrint = std.fmt.comptimePrint;
6 |
7 | /// Check if the given function (`Field`) is implemented for the `Actual` type.
8 | fn checkFunctionImplementation(
9 | comptime Interface: type,
10 | comptime Field: std.builtin.Type.StructField,
11 | comptime Actual: type,
12 | ) void {
13 | const Function = Field.type;
14 | const function = @typeInfo(Function);
15 |
16 | if (function != .Fn) @compileError("The Interface should only contains functions (as field)");
17 | if (function.Fn.is_generic) @compileError("Generic functions are not supported!");
18 | if (function.Fn.is_var_args) @compileError("Variadic functions are not supported!");
19 |
20 | const actual = @typeInfo(Actual);
21 | if (actual != .Struct) @compileError(comptimePrint("'{s}' should be a struct that implements '{s}'", .{
22 | @typeName(Actual),
23 | @typeName(Interface),
24 | }));
25 |
26 | inline for (actual.Struct.decls) |decl| {
27 | if (comptime std.mem.eql(u8, decl.name, Field.name)) {
28 | const decl_ = @field(Actual, decl.name);
29 | const Decl = @TypeOf(decl_);
30 | const decl_info = @typeInfo(Decl);
31 |
32 | if (decl_info != .Fn) @compileError(comptimePrint("Invalid Type for '{s}', should be {s}", .{
33 | Field.name,
34 | @typeName(Function),
35 | }));
36 | if (decl_info.Fn.is_generic or decl_info.Fn.is_var_args) @compileError(comptimePrint("Invalid Type for '{s}', should be {s}", .{
37 | Field.name,
38 | @typeName(Function),
39 | }));
40 |
41 | inline for (function.Fn.params, decl_info.Fn.params, 0..) |expected_param, actual_param, i| {
42 | if (i == 0) {
43 | if (expected_param.type == *const anyopaque) {
44 | if (actual_param.type != *const Actual) @compileError(comptimePrint("'self' (the 1st argument) should be of type '*const {s}'\nDefinition for '{s}':\n{s}", .{
45 | @typeName(Actual),
46 | Field.name,
47 | @typeName(Function),
48 | }));
49 | continue;
50 | } else if (expected_param.type == *anyopaque) {
51 | if (actual_param.type != *Actual) @compileError(comptimePrint("'self' (the 1st argument) should be of type '*{s}'\nDefinition for '{s}':\n{s}", .{
52 | @typeName(Actual),
53 | Field.name,
54 | @typeName(Function),
55 | }));
56 | continue;
57 | }
58 | }
59 |
60 | if (expected_param.type != actual_param.type) @compileError(comptimePrint("arg{d} is invalid, expected: {s}, given: {s}.\nDefinition for '{s}':\n{s}", .{
61 | i,
62 | @typeName(expected_param.type),
63 | @typeName(actual_param.type),
64 | Field.name,
65 | @typeName(Function),
66 | }));
67 | }
68 |
69 | if (function.Fn.return_type != decl_info.Fn.return_type) @compileError(comptimePrint("Invalid return type for '{s}', should be {s}\nDefinition for '{s}':\n{s}", .{
70 | Field.name,
71 | @typeName(Function),
72 | Field.name,
73 | @typeName(Function),
74 | }));
75 |
76 | return;
77 | }
78 | }
79 |
80 | @compileError(comptimePrint("'{s}' does not implement the function '{s}' and therefore does not meet the requirement of '{s}'.\nDefinition for '{s}':\n{s}", .{
81 | @typeName(Actual),
82 | Field.name,
83 | @typeName(Interface),
84 | Field.name,
85 | @typeName(Function),
86 | }));
87 | }
88 |
89 | /// Ensure that the `Actual` type implements the given `Interface` type.
90 | pub fn ensureImplement(
91 | comptime Interface: type,
92 | comptime Actual: type,
93 | ) void {
94 | const interface = @typeInfo(Interface);
95 |
96 | if (interface != .Struct) @compileError("The Interface should be a struct containing the functions as fields");
97 |
98 | inline for (interface.Struct.fields) |field| {
99 | checkFunctionImplementation(Interface, field, Actual);
100 | }
101 | }
--------------------------------------------------------------------------------
/src/main.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const SVG = @import("svg/SVG.zig");
4 |
5 | const Figure = @import("plot/Figure.zig");
6 | const Line = @import("plot/Line.zig");
7 | const Area = @import("plot/Area.zig");
8 | const Scatter = @import("plot/Scatter.zig");
9 |
10 | const Range = @import("util/range.zig").Range;
11 |
12 | /// The function for the 1st plot (area - blue)
13 | fn f(x: f32) f32 {
14 | if (x > 10.0) {
15 | return 20 - (2 * (x - 10.0));
16 | }
17 | return 2 * x;
18 | }
19 |
20 | /// The function for the 2nd plot (scatter - red)
21 | fn f2(x: f32) f32 {
22 | if (x > 10.0) {
23 | return 10.0;
24 | }
25 | return x;
26 | }
27 |
28 | /// The function for the 3rd plot (line - green)
29 | fn f3(x: f32) f32 {
30 | if (x < 8.0) {
31 | return 0.0;
32 | }
33 | return 0.5 * (x - 8.0);
34 | }
35 |
36 | pub fn main() !void {
37 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
38 | defer _ = gpa.deinit();
39 | const allocator = gpa.allocator();
40 |
41 | var points: [25]f32 = undefined;
42 | var points2: [25]f32 = undefined;
43 | var points3: [25]f32 = undefined;
44 | for (0..25) |i| {
45 | points[i] = f(@floatFromInt(i));
46 | points2[i] = f2(@floatFromInt(i));
47 | points3[i] = f3(@floatFromInt(i));
48 | }
49 |
50 | var figure = Figure.init(allocator, .{
51 | .title = .{
52 | .text = "Example plot",
53 | },
54 | });
55 | defer figure.deinit();
56 |
57 | try figure.addPlot(Area{ .y = &points, .style = .{
58 | .color = 0x0000FF,
59 | } });
60 | try figure.addPlot(Scatter{ .y = &points2, .style = .{
61 | .shape = .plus,
62 | .color = 0xFF0000,
63 | } });
64 | try figure.addPlot(Line{ .y = &points3, .style = .{
65 | .color = 0x00FF00,
66 | } });
67 | try figure.addPlot(Area{ .x = &[_]f32{ -5.0, 0.0, 5.0 }, .y = &[_]f32{ 5.0, 3.0, 5.0 }, .style = .{
68 | .color = 0xFF00FF,
69 | } });
70 | try figure.addPlot(Area{ .x = &[_]f32{ -5.0, 0.0, 5.0 }, .y = &[_]f32{ -5.0, -3.0, -5.0 }, .style = .{
71 | .color = 0xFFFF00,
72 | } });
73 |
74 | var svg = try figure.show();
75 | defer svg.deinit();
76 |
77 | // Write to an output file (out.svg)
78 | var file = try std.fs.cwd().createFile("out.svg", .{});
79 | defer file.close();
80 |
81 | try svg.writeTo(file.writer());
82 | }
83 |
--------------------------------------------------------------------------------
/src/plot/Area.zig:
--------------------------------------------------------------------------------
1 | //! The Area plot
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | const SVG = @import("../svg/SVG.zig");
7 | const RGB = @import("../svg/util/rgb.zig").RGB;
8 | const Range = @import("../util/range.zig").Range;
9 |
10 | const Plot = @import("Plot.zig");
11 | const FigureInfo = @import("FigureInfo.zig");
12 |
13 | const Area = @This();
14 |
15 | /// The Style of the Area plot
16 | pub const Style = struct {
17 | /// The title of the plot
18 | title: ?[]const u8 = null,
19 | /// The color of the area
20 | color: RGB = 0x0000FF,
21 | /// The opacity of the area
22 | opacity: f32 = 0.5,
23 | /// The width of the line
24 | width: f32 = 2.0,
25 | };
26 |
27 | /// The x-axis values of the area plot
28 | x: ?[]const f32 = null,
29 | /// The y-axis values of the area plot
30 | y: []const f32,
31 | /// The style of the area plot
32 | style: Style = .{},
33 |
34 | /// Returns the range of the x values of the line plot
35 | fn getXRange(impl: *const anyopaque) Range(f32) {
36 | const self: *const Area = @ptrCast(@alignCast(impl));
37 | if (self.x) |x| {
38 | const min_max = std.mem.minMax(f32, x);
39 | return Range(f32) {
40 | .min = min_max.@"0",
41 | .max = min_max.@"1",
42 | };
43 | } else {
44 | return Range(f32) {
45 | .min = 0.0,
46 | .max = @floatFromInt(self.y.len - 1),
47 | };
48 | }
49 | }
50 |
51 | /// Returns the range of the y values of the line plot
52 | fn getYRange(impl: *const anyopaque) Range(f32) {
53 | const self: *const Area = @ptrCast(@alignCast(impl));
54 | const min_max = std.mem.minMax(f32, self.y);
55 | return Range(f32) {
56 | .min = min_max.@"0",
57 | .max = min_max.@"1",
58 | };
59 | }
60 |
61 | /// The draw function for the area plot (converts the plot to SVG)
62 | fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) !void {
63 | const self: *const Area = @ptrCast(@alignCast(impl));
64 |
65 | if (self.x) |x_| {
66 | var points = std.ArrayList(f32).init(allocator);
67 | try points.appendSlice(&[_]f32 {info.computeX(x_[0]), info.getBaseY()});
68 | var last_x: ?f32 = null;
69 | for (x_, self.y) |x, y| {
70 | if (!info.x_range.contains(x)) continue;
71 | if (!info.y_range.contains(y)) continue;
72 |
73 | if (last_x) |last_x_| {
74 | if (x > last_x_) last_x = x;
75 | } else last_x = x;
76 |
77 | const x2 = info.computeX(x);
78 | const y2 = info.computeY(y);
79 |
80 | try points.append(x2);
81 | try points.append(y2);
82 | }
83 |
84 | if (last_x) |last_x_| try points.appendSlice(&[_]f32 {info.computeX(last_x_), info.getBaseY()});
85 | try svg.addPolyline(.{
86 | .points = try points.toOwnedSlice(),
87 | .fill = self.style.color,
88 | .fill_opacity = self.style.opacity,
89 | .stroke = self.style.color,
90 | .stroke_width = .{ .pixel = self.style.width },
91 | });
92 | } else {
93 | var points = std.ArrayList(f32).init(allocator);
94 | try points.appendSlice(&[_]f32 {info.computeX(0.0), info.getBaseY()});
95 | var last_x: ?f32 = null;
96 | for (self.y, 0..) |y, x| {
97 | if (!info.x_range.contains(@floatFromInt(x))) continue;
98 | if (!info.y_range.contains(y)) continue;
99 |
100 | if (last_x) |last_x_| {
101 | if (@as(f32, @floatFromInt(x)) > last_x_) last_x = @floatFromInt(x);
102 | } else last_x = @floatFromInt(x);
103 |
104 | const x2 = info.computeX(@floatFromInt(x));
105 | const y2 = info.computeY(y);
106 |
107 | try points.append(x2);
108 | try points.append(y2);
109 | }
110 |
111 | if (last_x) |last_x_| try points.appendSlice(&[_]f32 {info.computeX(last_x_), info.getBaseY()});
112 | try svg.addPolyline(.{
113 | .points = try points.toOwnedSlice(),
114 | .fill = self.style.color,
115 | .fill_opacity = self.style.opacity,
116 | .stroke = self.style.color,
117 | .stroke_width = .{ .pixel = self.style.width },
118 | });
119 | }
120 | }
121 |
122 | /// Converts the area plot to a plot (its interface)
123 | pub fn interface(self: *const Area) Plot {
124 | return Plot.init(
125 | @as(*const anyopaque, self),
126 | self.style.title,
127 | self.style.color,
128 | &getXRange,
129 | &getYRange,
130 | &draw
131 | );
132 | }
--------------------------------------------------------------------------------
/src/plot/CandleStick.zig:
--------------------------------------------------------------------------------
1 | //! The Candle Stick plot
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | const SVG = @import("../svg/SVG.zig");
7 | const RGB = @import("../svg/util/rgb.zig").RGB;
8 | const Range = @import("../util/range.zig").Range;
9 |
10 | const Plot = @import("Plot.zig");
11 | const FigureInfo = @import("FigureInfo.zig");
12 |
13 | const Step = @This();
14 |
15 | /// A candle
16 | pub const Candle = struct {
17 | open: f32,
18 | close: f32,
19 | high: f32,
20 | low: f32,
21 | color: ?RGB = null,
22 | };
23 |
24 | /// The style of the candle stick plot
25 | pub const Style = struct {
26 | /// The title of the plot
27 | title: ?[]const u8 = null,
28 | /// The color of the bar when it increases
29 | inc_color: RGB = 0x00FF00,
30 | /// The color of the bar when it decreases
31 | dec_color: RGB = 0xFF0000,
32 | /// The width of the bars
33 | width: f32 = 8.0,
34 | /// The gap between the bars
35 | gap: f32 = 2.0,
36 | /// The thickness of the line
37 | line_thickness: f32 = 2.0,
38 | };
39 |
40 | /// The y-axis values of the candle stick plot
41 | candles: []Candle,
42 | /// The style of the candle stick plot
43 | style: Style = .{},
44 |
45 | /// Returns the range of the x values of the step plot
46 | fn getXRange(impl: *const anyopaque) Range(f32) {
47 | const self: *const Step = @ptrCast(@alignCast(impl));
48 |
49 | return Range(f32){
50 | .min = 0.0,
51 | .max = @as(f32, @floatFromInt(self.candles.len)) * (self.style.width + self.style.gap),
52 | };
53 | }
54 |
55 | /// Returns the range of the y values of the step plot
56 | fn getYRange(impl: *const anyopaque) Range(f32) {
57 | const self: *const Step = @ptrCast(@alignCast(impl));
58 |
59 | var min: f32 = std.math.inf(f32);
60 | var max: f32 = 0;
61 | for (self.candles) |y| {
62 | if (y.low < min) min = y.low;
63 | if (y.high > max) max = y.high;
64 | }
65 |
66 | return Range(f32){
67 | .min = min,
68 | .max = max,
69 | };
70 | }
71 |
72 | /// Draws the candle stick plot (converts to SVG)
73 | fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) !void {
74 | const self: *const Step = @ptrCast(@alignCast(impl));
75 | _ = allocator;
76 |
77 | const gap_x = info.computeX(self.style.gap);
78 | for (self.candles, 0..) |y, x| {
79 | const x_left = info.computeX(@as(f32, @floatFromInt(x)) * (self.style.width + self.style.gap));
80 | const x_right = info.computeX(@as(f32, @floatFromInt(x + 1)) * (self.style.width + self.style.gap));
81 | const y_open = info.computeY(y.open);
82 | const y_close = info.computeY(y.close);
83 | const y_high = info.computeY(y.high);
84 | const y_low = info.computeY(y.low);
85 |
86 | const color = y.color orelse if (y.open > y.close) self.style.dec_color else self.style.inc_color;
87 |
88 | try svg.addLine(.{
89 | .x1 = .{ .pixel = (x_left + x_right) / 2 },
90 | .y1 = .{ .pixel = y_high },
91 | .x2 = .{ .pixel = (x_left + x_right) / 2 },
92 | .y2 = .{ .pixel = y_low },
93 | .stroke = color,
94 | .stroke_width = .{ .pixel = self.style.line_thickness },
95 | .stroke_linecap = SVG.Line.LineCap.round,
96 | });
97 |
98 | try svg.addRect(.{
99 | .x = .{ .pixel = x_left + gap_x / 2 },
100 | .y = .{ .pixel = @min(y_open, y_close) },
101 | .width = .{ .pixel = x_right - x_left - gap_x },
102 | .height = .{ .pixel = @abs(y_close - y_open) },
103 | .fill = color,
104 | });
105 | }
106 | }
107 |
108 | /// Convert the Step Plot to a Plot (its interface)
109 | pub fn interface(self: *const Step) Plot {
110 | return Plot.init(
111 | @as(*const anyopaque, self),
112 | self.style.title,
113 | self.style.inc_color,
114 | &getXRange,
115 | &getYRange,
116 | &draw,
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/plot/FigureInfo.zig:
--------------------------------------------------------------------------------
1 | //! The info of a Figure shared between the plots.
2 |
3 | const std = @import("std");
4 |
5 | const Range = @import("../util/range.zig").Range;
6 | const Scale = @import("../util/scale.zig").Scale;
7 |
8 | const FigureInfo = @This();
9 |
10 | /// The width of the plot (in pixels).
11 | /// Note that this is not the width of the figure, but only the width of the plot.
12 | width: f32,
13 | /// The height of the plot (in pixels).
14 | /// Note that this is not the height of the figure, but only the height of the plot.
15 | height: f32,
16 | /// The range of the x axis.
17 | x_range: Range(f32),
18 | /// The range of the y axis.
19 | y_range: Range(f32),
20 | /// The scale for the values on the x-axis
21 | x_scale: Scale,
22 | /// The scale for the value on the y-axis
23 | y_scale: Scale,
24 |
25 | /// Get the delta x of the figure.
26 | pub fn getDx(self: *const FigureInfo) f32 {
27 | return self.width / (self.x_range.max - self.x_range.min);
28 | }
29 |
30 | /// Get the delta y of the figure.
31 | pub fn getDy(self: *const FigureInfo) f32 {
32 | return self.height / (self.y_range.max - self.y_range.min);
33 | }
34 |
35 | /// Convert a value in the linear range into the log10 range.
36 | pub fn linearToLog10(min: f32, max: f32, x: f32) f32 {
37 | return (@log10(x) - @log10(min)) / (@log10(max) - @log10(min)) * (max - min);
38 | }
39 |
40 | /// Compute the x coordinate of a point in the figure
41 | pub fn computeX(self: *const FigureInfo, x: f32) f32 {
42 | return switch (self.x_scale) {
43 | .linear => (x - self.x_range.min) * self.getDx(),
44 | .log => linearToLog10(self.x_range.min, self.x_range.max, x) * self.getDx(),
45 | };
46 | }
47 |
48 | /// Compute the y coordinate of a point in the figure
49 | pub fn computeY(self: *const FigureInfo, y: f32) f32 {
50 | return switch (self.y_scale) {
51 | .linear => self.height - (y - self.y_range.min) * self.getDy(),
52 | .log => self.height - linearToLog10(self.y_range.min, self.y_range.max, y) * self.getDy(),
53 | };
54 | }
55 |
56 | /// Compute the inverse x coordinate of a point in the figure
57 | pub fn computeXInv(self: *const FigureInfo, x: f32) f32 {
58 | return x / self.getDx() + self.x_range.min;
59 | }
60 |
61 | /// Compute the inverse y coordinate of a point in the figure
62 | pub fn computeYInv(self: *const FigureInfo, y: f32) f32 {
63 | return (self.height - y) / self.getDy() + self.y_range.min;
64 | }
65 |
66 | /// Get the base-y coordinate (0.0, or minimum, or maximum)
67 | pub fn getBaseY(self: *const FigureInfo) f32 {
68 | if (self.y_range.contains(0)) return self.computeY(0.0) else if (self.y_range.min < 0.0) return self.computeY(self.y_range.max) else return self.computeY(self.y_range.min);
69 | }
70 |
71 | /// Get the base-x coordinate (0.0, or minimum, or maximum)
72 | pub fn getBaseX(self: *const FigureInfo) f32 {
73 | if (self.x_range.contains(0)) return self.computeX(0.0) else if (self.x_range.min < 0.0) return self.computeX(self.x_range.max) else return self.computeX(self.x_range.min);
74 | }
75 |
76 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
77 | // Tests for "compute Δx" //
78 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
79 |
80 | test "compute Δx - Positive Zero" {
81 | const info = FigureInfo{
82 | .width = 100.0,
83 | .height = 100.0,
84 | .x_range = Range(f32).init(0.0, 10.0),
85 | .y_range = Range(f32).init(0.0, 0.0),
86 | .x_scale = .linear,
87 | .y_scale = .linear,
88 | };
89 |
90 | const dx = info.getDx();
91 |
92 | try std.testing.expectEqual(@as(f32, 10.0), dx);
93 | }
94 |
95 | test "compute Δx - Positive" {
96 | const info = FigureInfo{
97 | .width = 100.0,
98 | .height = 100.0,
99 | .x_range = Range(f32).init(5.0, 10.0),
100 | .y_range = Range(f32).init(0.0, 0.0),
101 | .x_scale = .linear,
102 | .y_scale = .linear,
103 | };
104 |
105 | const dx = info.getDx();
106 |
107 | try std.testing.expectEqual(@as(f32, 20.0), dx);
108 | }
109 |
110 | test "compute Δx - Negative Zero" {
111 | const info = FigureInfo{
112 | .width = 100.0,
113 | .height = 100.0,
114 | .x_range = Range(f32).init(-10.0, 0.0),
115 | .y_range = Range(f32).init(0.0, 0.0),
116 | .x_scale = .linear,
117 | .y_scale = .linear,
118 | };
119 |
120 | const dx = info.getDx();
121 |
122 | try std.testing.expectEqual(@as(f32, 10.0), dx);
123 | }
124 |
125 | test "compute Δx - Negative" {
126 | const info = FigureInfo{
127 | .width = 100.0,
128 | .height = 100.0,
129 | .x_range = Range(f32).init(-10.0, -5.0),
130 | .y_range = Range(f32).init(0.0, 0.0),
131 | .x_scale = .linear,
132 | .y_scale = .linear,
133 | };
134 |
135 | const dx = info.getDx();
136 |
137 | try std.testing.expectEqual(@as(f32, 20.0), dx);
138 | }
139 |
140 | test "compute Δx - Positive & Negative" {
141 | const info = FigureInfo{
142 | .width = 100.0,
143 | .height = 100.0,
144 | .x_range = Range(f32).init(-10.0, 10.0),
145 | .y_range = Range(f32).init(0.0, 0.0),
146 | .x_scale = .linear,
147 | .y_scale = .linear,
148 | };
149 |
150 | const dx = info.getDx();
151 |
152 | try std.testing.expectEqual(@as(f32, 5.0), dx);
153 | }
154 |
155 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
156 | // Tests for "compute Δy" //
157 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
158 |
159 | test "compute Δy - Positive Zero" {
160 | const info = FigureInfo{
161 | .width = 100.0,
162 | .height = 100.0,
163 | .x_range = Range(f32).init(0.0, 0.0),
164 | .y_range = Range(f32).init(0.0, 10.0),
165 | .x_scale = .linear,
166 | .y_scale = .linear,
167 | };
168 |
169 | const dy = info.getDy();
170 |
171 | try std.testing.expectEqual(@as(f32, 10.0), dy);
172 | }
173 |
174 | test "compute Δy - Positive" {
175 | const info = FigureInfo{
176 | .width = 100.0,
177 | .height = 100.0,
178 | .x_range = Range(f32).init(0.0, 0.0),
179 | .y_range = Range(f32).init(5.0, 10.0),
180 | .x_scale = .linear,
181 | .y_scale = .linear,
182 | };
183 |
184 | const dy = info.getDy();
185 |
186 | try std.testing.expectEqual(@as(f32, 20.0), dy);
187 | }
188 |
189 | test "compute Δy - Negative Zero" {
190 | const info = FigureInfo{
191 | .width = 100.0,
192 | .height = 100.0,
193 | .x_range = Range(f32).init(0.0, 0.0),
194 | .y_range = Range(f32).init(-10.0, 0.0),
195 | .x_scale = .linear,
196 | .y_scale = .linear,
197 | };
198 |
199 | const dy = info.getDy();
200 |
201 | try std.testing.expectEqual(@as(f32, 10.0), dy);
202 | }
203 |
204 | test "compute Δy - Negative" {
205 | const info = FigureInfo{
206 | .width = 100.0,
207 | .height = 100.0,
208 | .x_range = Range(f32).init(0.0, 0.0),
209 | .y_range = Range(f32).init(-10.0, -5.0),
210 | .x_scale = .linear,
211 | .y_scale = .linear,
212 | };
213 |
214 | const dy = info.getDy();
215 |
216 | try std.testing.expectEqual(@as(f32, 20.0), dy);
217 | }
218 |
219 | test "compute Δy - Positive & Negative" {
220 | const info = FigureInfo{
221 | .width = 100.0,
222 | .height = 100.0,
223 | .x_range = Range(f32).init(0.0, 0.0),
224 | .y_range = Range(f32).init(-10.0, 10.0),
225 | .x_scale = .linear,
226 | .y_scale = .linear,
227 | };
228 |
229 | const dy = info.getDy();
230 |
231 | try std.testing.expectEqual(@as(f32, 5.0), dy);
232 | }
233 |
234 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
235 | // Tests for "compute x" //
236 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
237 |
238 | test "compute x - Positive Zero" {
239 | const info = FigureInfo{
240 | .width = 100.0,
241 | .height = 100.0,
242 | .x_range = Range(f32).init(0.0, 10.0),
243 | .y_range = Range(f32).init(0.0, 0.0),
244 | .x_scale = .linear,
245 | .y_scale = .linear,
246 | };
247 |
248 | // start of the range
249 | const x_start = info.computeX(0.0);
250 | try std.testing.expectEqual(@as(f32, 0.0), x_start);
251 |
252 | // Middle of the range
253 | const x_middle = info.computeX(5.0);
254 | try std.testing.expectEqual(@as(f32, 50.0), x_middle);
255 |
256 | // End of the range
257 | const x_end = info.computeX(10.0);
258 | try std.testing.expectEqual(@as(f32, 100.0), x_end);
259 | }
260 |
261 | test "compute x - Positive" {
262 | const info = FigureInfo{
263 | .width = 100.0,
264 | .height = 100.0,
265 | .x_range = Range(f32).init(5.0, 10.0),
266 | .y_range = Range(f32).init(0.0, 0.0),
267 | .x_scale = .linear,
268 | .y_scale = .linear,
269 | };
270 |
271 | // start of the range
272 | const x_start = info.computeX(5.0);
273 | try std.testing.expectEqual(@as(f32, 0.0), x_start);
274 |
275 | // Middle of the range
276 | const x_middle = info.computeX(7.5);
277 | try std.testing.expectEqual(@as(f32, 50.0), x_middle);
278 |
279 | // End of the range
280 | const x_end = info.computeX(10.0);
281 | try std.testing.expectEqual(@as(f32, 100.0), x_end);
282 | }
283 |
284 | test "compute x - Negative Zero" {
285 | const info = FigureInfo{
286 | .width = 100.0,
287 | .height = 100.0,
288 | .x_range = Range(f32).init(-10.0, 0.0),
289 | .y_range = Range(f32).init(0.0, 0.0),
290 | .x_scale = .linear,
291 | .y_scale = .linear,
292 | };
293 |
294 | // start of the range
295 | const x_start = info.computeX(-10.0);
296 | try std.testing.expectEqual(@as(f32, 0.0), x_start);
297 |
298 | // Middle of the range
299 | const x_middle = info.computeX(-5.0);
300 | try std.testing.expectEqual(@as(f32, 50.0), x_middle);
301 |
302 | // End of the range
303 | const x_end = info.computeX(0.0);
304 | try std.testing.expectEqual(@as(f32, 100.0), x_end);
305 | }
306 |
307 | test "compute x - Negative" {
308 | const info = FigureInfo{
309 | .width = 100.0,
310 | .height = 100.0,
311 | .x_range = Range(f32).init(-10.0, -5.0),
312 | .y_range = Range(f32).init(0.0, 0.0),
313 | .x_scale = .linear,
314 | .y_scale = .linear,
315 | };
316 |
317 | // start of the range
318 | const x_start = info.computeX(-10.0);
319 | try std.testing.expectEqual(@as(f32, 0.0), x_start);
320 |
321 | // Middle of the range
322 | const x_middle = info.computeX(-7.5);
323 | try std.testing.expectEqual(@as(f32, 50.0), x_middle);
324 |
325 | // End of the range
326 | const x_end = info.computeX(-5.0);
327 | try std.testing.expectEqual(@as(f32, 100.0), x_end);
328 | }
329 |
330 | test "compute x - Positive & Negative" {
331 | const info = FigureInfo{
332 | .width = 100.0,
333 | .height = 100.0,
334 | .x_range = Range(f32).init(-10.0, 10.0),
335 | .y_range = Range(f32).init(0.0, 0.0),
336 | .x_scale = .linear,
337 | .y_scale = .linear,
338 | };
339 |
340 | // start of the range
341 | const x_start = info.computeX(-10.0);
342 | try std.testing.expectEqual(@as(f32, 0.0), x_start);
343 |
344 | // Middle of the range
345 | const x_middle = info.computeX(0.0);
346 | try std.testing.expectEqual(@as(f32, 50.0), x_middle);
347 |
348 | // End of the range
349 | const x_end = info.computeX(10.0);
350 | try std.testing.expectEqual(@as(f32, 100.0), x_end);
351 | }
352 |
353 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
354 | // Tests for "compute y" //
355 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
356 |
357 | test "compute y - Positive Zero" {
358 | const info = FigureInfo{
359 | .width = 100.0,
360 | .height = 100.0,
361 | .x_range = Range(f32).init(0.0, 0.0),
362 | .y_range = Range(f32).init(0.0, 10.0),
363 | .x_scale = .linear,
364 | .y_scale = .linear,
365 | };
366 |
367 | // start of the range
368 | const y_start = info.computeY(0.0);
369 | try std.testing.expectEqual(@as(f32, 100.0), y_start);
370 |
371 | // Middle of the range
372 | const y_middle = info.computeY(5.0);
373 | try std.testing.expectEqual(@as(f32, 50.0), y_middle);
374 |
375 | // End of the range
376 | const y_end = info.computeY(10.0);
377 | try std.testing.expectEqual(@as(f32, 0.0), y_end);
378 | }
379 |
380 | test "compute y - Positive" {
381 | const info = FigureInfo{
382 | .width = 100.0,
383 | .height = 100.0,
384 | .x_range = Range(f32).init(0.0, 0.0),
385 | .y_range = Range(f32).init(5.0, 10.0),
386 | .x_scale = .linear,
387 | .y_scale = .linear,
388 | };
389 |
390 | // start of the range
391 | const y_start = info.computeY(5.0);
392 | try std.testing.expectEqual(@as(f32, 100.0), y_start);
393 |
394 | // Middle of the range
395 | const y_middle = info.computeY(7.5);
396 | try std.testing.expectEqual(@as(f32, 50.0), y_middle);
397 |
398 | // End of the range
399 | const y_end = info.computeY(10.0);
400 | try std.testing.expectEqual(@as(f32, 0.0), y_end);
401 | }
402 |
403 | test "compute y - Negative Zero" {
404 | const info = FigureInfo{
405 | .width = 100.0,
406 | .height = 100.0,
407 | .x_range = Range(f32).init(0.0, 0.0),
408 | .y_range = Range(f32).init(-10.0, 0.0),
409 | .x_scale = .linear,
410 | .y_scale = .linear,
411 | };
412 |
413 | // start of the range
414 | const y_start = info.computeY(-10.0);
415 | try std.testing.expectEqual(@as(f32, 100.0), y_start);
416 |
417 | // Middle of the range
418 | const y_middle = info.computeY(-5.0);
419 | try std.testing.expectEqual(@as(f32, 50.0), y_middle);
420 |
421 | // End of the range
422 | const y_end = info.computeY(0.0);
423 | try std.testing.expectEqual(@as(f32, 0.0), y_end);
424 | }
425 |
426 | test "compute y - Negative" {
427 | const info = FigureInfo{
428 | .width = 100.0,
429 | .height = 100.0,
430 | .x_range = Range(f32).init(0.0, 0.0),
431 | .y_range = Range(f32).init(-10.0, -5.0),
432 | .x_scale = .linear,
433 | .y_scale = .linear,
434 | };
435 |
436 | // start of the range
437 | const y_start = info.computeY(-10.0);
438 | try std.testing.expectEqual(@as(f32, 100.0), y_start);
439 |
440 | // Middle of the range
441 | const y_middle = info.computeY(-7.5);
442 | try std.testing.expectEqual(@as(f32, 50.0), y_middle);
443 |
444 | // End of the range
445 | const y_end = info.computeY(-5.0);
446 | try std.testing.expectEqual(@as(f32, 0.0), y_end);
447 | }
448 |
449 | test "compute y - Positive & Negative" {
450 | const info = FigureInfo{
451 | .width = 100.0,
452 | .height = 100.0,
453 | .x_range = Range(f32).init(0.0, 0.0),
454 | .y_range = Range(f32).init(-10.0, 10.0),
455 | .x_scale = .linear,
456 | .y_scale = .linear,
457 | };
458 |
459 | // start of the range
460 | const y_start = info.computeY(-10.0);
461 | try std.testing.expectEqual(@as(f32, 100.0), y_start);
462 |
463 | // Middle of the range
464 | const y_middle = info.computeY(0.0);
465 | try std.testing.expectEqual(@as(f32, 50.0), y_middle);
466 |
467 | // End of the range
468 | const y_end = info.computeY(10.0);
469 | try std.testing.expectEqual(@as(f32, 0.0), y_end);
470 | }
471 |
--------------------------------------------------------------------------------
/src/plot/Line.zig:
--------------------------------------------------------------------------------
1 | //! The Line plot
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | const SVG = @import("../svg/SVG.zig");
7 | const RGB = @import("../svg/util/rgb.zig").RGB;
8 | const Range = @import("../util/range.zig").Range;
9 |
10 | const Plot = @import("Plot.zig");
11 | const FigureInfo = @import("FigureInfo.zig");
12 |
13 | const Line = @This();
14 |
15 | /// The style of the line plot
16 | pub const Style = struct {
17 | /// The title of the plot
18 | title: ?[]const u8 = null,
19 | /// The color of the line
20 | color: RGB = 0x0000FF,
21 | /// The width of the line
22 | width: f32 = 2.0,
23 | /// The size of the dashes of the line (null = no dashes)
24 | dash: ?f32 = null,
25 | /// The smoothing factor [0; 1] (0 = no smoothing)
26 | smooth: f32 = 0.0,
27 | };
28 |
29 | /// The x-axis values of the line plot
30 | x: ?[]const f32 = null,
31 | /// The y-axis values of the line plot
32 | y: []const f32,
33 | /// The style of the line plot
34 | style: Style = .{},
35 |
36 | /// Returns the range of the x values of the line plot
37 | fn getXRange(impl: *const anyopaque) Range(f32) {
38 | const self: *const Line = @ptrCast(@alignCast(impl));
39 | if (self.x) |x| {
40 | const min_max = std.mem.minMax(f32, x);
41 | return Range(f32){
42 | .min = min_max.@"0",
43 | .max = min_max.@"1",
44 | };
45 | } else {
46 | return Range(f32){
47 | .min = 0.0,
48 | .max = if (self.y.len == 0) 0 else @floatFromInt(self.y.len - 1),
49 | };
50 | }
51 | }
52 |
53 | /// Returns the range of the y values of the line plot
54 | fn getYRange(impl: *const anyopaque) Range(f32) {
55 | const self: *const Line = @ptrCast(@alignCast(impl));
56 | const min_max = std.mem.minMax(f32, self.y);
57 | return Range(f32){
58 | .min = min_max.@"0",
59 | .max = min_max.@"1",
60 | };
61 | }
62 |
63 | /// Draws the line plot (converts to SVG)
64 | fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) !void {
65 | const self: *const Line = @ptrCast(@alignCast(impl));
66 |
67 | const stroke_dash_array: ?[]const f32 = if (self.style.dash) |dash| try allocator.dupe(f32, &[_]f32{dash}) else null;
68 |
69 | var commands = std.ArrayList(SVG.Path.Command).init(allocator);
70 | var started = false;
71 | if (self.x) |x_| {
72 | for (x_, self.y, 0..) |x, y, i| {
73 | if (!info.x_range.contains(x) or !info.y_range.contains(y)) {
74 | continue;
75 | }
76 |
77 | const x1 = info.computeX(x);
78 | const y1 = info.computeY(y);
79 |
80 | if (!started) {
81 | try commands.append(.{
82 | .MoveTo = .{
83 | .x = x1,
84 | .y = y1,
85 | },
86 | });
87 | started = true;
88 | continue;
89 | }
90 |
91 | const p_start_x = info.computeX(x_[i - 1]);
92 | const p_start_y = info.computeY(self.y[i - 1]);
93 | const p_end_x = info.computeX(x);
94 | const p_end_y = info.computeY(y);
95 |
96 | const p_prev_x = if (i >= 2) info.computeX(x_[i - 2]) else p_start_x;
97 | const p_prev_y = if (i >= 2) info.computeY(self.y[i - 2]) else p_start_y;
98 | const p_next_x = if (i + 1 < x_.len) info.computeX(x_[i + 1]) else p_end_x;
99 | const p_next_y = if (i + 1 < self.y.len) info.computeY(self.y[i + 1]) else p_end_y;
100 |
101 | const cps_x = p_start_x + self.style.smooth * (p_end_x - p_prev_x);
102 | const cps_y = p_start_y + self.style.smooth * (p_end_y - p_prev_y);
103 |
104 | const cpe_x = p_end_x + self.style.smooth * (p_start_x - p_next_x);
105 | const cpe_y = p_end_y + self.style.smooth * (p_start_y - p_next_y);
106 |
107 | try commands.append(.{
108 | .CubicBezierCurveTo = .{
109 | .x1 = cps_x,
110 | .y1 = cps_y,
111 | .x2 = cpe_x,
112 | .y2 = cpe_y,
113 | .x = x1,
114 | .y = y1,
115 | },
116 | });
117 | }
118 | } else {
119 | for (self.y, 0..) |y, x| {
120 | if (!info.x_range.contains(@floatFromInt(x)) or !info.y_range.contains(y)) {
121 | continue;
122 | }
123 |
124 | const x1 = info.computeX(@floatFromInt(x));
125 | const y1 = info.computeY(y);
126 |
127 | if (!started) {
128 | try commands.append(.{
129 | .MoveTo = .{
130 | .x = x1,
131 | .y = y1,
132 | },
133 | });
134 | started = true;
135 | continue;
136 | }
137 |
138 | const p_start_x: f32 = info.computeX(@floatFromInt(x - 1));
139 | const p_start_y = info.computeY(self.y[x - 1]);
140 | const p_end_x: f32 = info.computeX(@floatFromInt(x));
141 | const p_end_y = info.computeY(y);
142 |
143 | const p_prev_x: f32 = if (x >= 2) info.computeX(@floatFromInt(x - 1)) else p_start_x;
144 | const p_prev_y = if (x >= 2) info.computeY(self.y[x - 2]) else p_start_y;
145 | const p_next_x: f32 = if (x + 1 < self.y.len) info.computeX(@floatFromInt(x + 1)) else p_end_x;
146 | const p_next_y = if (x + 1 < self.y.len) info.computeY(self.y[x + 1]) else p_end_y;
147 |
148 | const cps_x = p_start_x + self.style.smooth * (p_end_x - p_prev_x);
149 | const cps_y = p_start_y + self.style.smooth * (p_end_y - p_prev_y);
150 |
151 | const cpe_x = p_end_x + self.style.smooth * (p_start_x - p_next_x);
152 | const cpe_y = p_end_y + self.style.smooth * (p_start_y - p_next_y);
153 |
154 | try commands.append(.{
155 | .CubicBezierCurveTo = .{
156 | .x1 = cps_x,
157 | .y1 = cps_y,
158 | .x2 = cpe_x,
159 | .y2 = cpe_y,
160 | .x = x1,
161 | .y = y1,
162 | },
163 | });
164 | }
165 | }
166 |
167 | try svg.addPath(.{
168 | .commands = commands.items,
169 | .allocator = allocator,
170 | .stroke = self.style.color,
171 | .stroke_width = .{ .pixel = self.style.width },
172 | .stroke_dasharray = stroke_dash_array,
173 | });
174 | }
175 |
176 | /// Convert the Line Plot to a Plot (its interface)
177 | pub fn interface(self: *const Line) Plot {
178 | return Plot.init(
179 | @as(*const anyopaque, self),
180 | self.style.title,
181 | self.style.color,
182 | &getXRange,
183 | &getYRange,
184 | &draw,
185 | );
186 | }
187 |
--------------------------------------------------------------------------------
/src/plot/Marker.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const SVG = @import("../svg/SVG.zig");
5 | const FigureInfo = @import("FigureInfo.zig");
6 |
7 | const Marker = @This();
8 |
9 | ////////////////////////////////////////////////////////////////////////////////////////////////////
10 | // Marker //
11 | ////////////////////////////////////////////////////////////////////////////////////////////////////
12 |
13 | /// A List of markers
14 | pub const List = std.ArrayList(Marker);
15 |
16 | /// The type of the draw function
17 | const DrawFn = fn (*const anyopaque, allocator: Allocator, *SVG, FigureInfo) anyerror!void;
18 |
19 | /// The implementation of the marker
20 | impl: *const anyopaque,
21 |
22 | /// The draw function of the marker
23 | draw_fn: *const DrawFn,
24 |
25 | /// Initialize a marker with the implementation and the draw function.
26 | pub fn init(impl: *const anyopaque, draw_fn: *const DrawFn) Marker {
27 | return Marker{
28 | .impl = impl,
29 | .draw_fn = draw_fn,
30 | };
31 | }
32 |
33 | /// Draws the marker
34 | pub fn draw(self: *const Marker, allocator: Allocator, svg: *SVG, info: FigureInfo) anyerror!void {
35 | try self.draw_fn(self.impl, allocator, svg, info);
36 | }
37 |
--------------------------------------------------------------------------------
/src/plot/Plot.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const SVG = @import("../svg/SVG.zig");
5 | const Range = @import("../util/range.zig").Range;
6 | const RGB = @import("../svg/util/rgb.zig").RGB;
7 |
8 | const FigureInfo = @import("FigureInfo.zig");
9 |
10 | const Plot = @This();
11 |
12 | /// A List of Plot
13 | pub const List = std.ArrayList(Plot);
14 |
15 | impl: *const anyopaque,
16 | title: ?[]const u8,
17 | color: RGB,
18 | get_range_x_fn: *const fn(*const anyopaque) Range(f32),
19 | get_range_y_fn: *const fn(*const anyopaque) Range(f32),
20 | draw_fn: *const fn(*const anyopaque, allocator: Allocator, *SVG, FigureInfo) anyerror!void,
21 |
22 | pub fn init(
23 | impl: *const anyopaque,
24 | title: ?[]const u8,
25 | color: RGB,
26 | get_range_x_fn: *const fn(*const anyopaque) Range(f32),
27 | get_range_y_fn: *const fn(*const anyopaque) Range(f32),
28 | draw_fn: *const fn(*const anyopaque, Allocator, *SVG, FigureInfo) anyerror!void,
29 | ) Plot {
30 | return Plot {
31 | .impl = impl,
32 | .title = title,
33 | .color = color,
34 | .get_range_x_fn = get_range_x_fn,
35 | .get_range_y_fn = get_range_y_fn,
36 | .draw_fn = draw_fn,
37 | };
38 | }
39 |
40 | pub fn getRangeX(self: *const Plot) Range(f32) {
41 | return self.get_range_x_fn(self.impl);
42 | }
43 |
44 | pub fn getRangeY(self: *const Plot) Range(f32) {
45 | return self.get_range_y_fn(self.impl);
46 | }
47 |
48 | pub fn draw(self: *const Plot, allocator: Allocator, svg: *SVG, info: FigureInfo) anyerror!void {
49 | try self.draw_fn(self.impl, allocator, svg, info);
50 | }
--------------------------------------------------------------------------------
/src/plot/Scatter.zig:
--------------------------------------------------------------------------------
1 | //! The Scatter Plot
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | const SVG = @import("../svg/SVG.zig");
7 | const RGB = @import("../svg/util/rgb.zig").RGB;
8 | const Range = @import("../util/range.zig").Range;
9 |
10 | const Shape = @import("../util/shape.zig").Shape;
11 |
12 | const Plot = @import("Plot.zig");
13 | const FigureInfo = @import("FigureInfo.zig");
14 |
15 | const Scatter = @This();
16 |
17 | /// The style of the scatter plot
18 | pub const Style = struct {
19 | /// The title of the plot
20 | title: ?[]const u8 = null,
21 | /// The color of the line
22 | color: RGB = 0x0000FF,
23 | /// The width of the line
24 | radius: f32 = 2.0,
25 | /// The shape of the points
26 | shape: Shape = .circle,
27 | };
28 |
29 | /// The x-axis value of the scatter plot
30 | x: ?[]const f32 = null,
31 | /// The y-axis value of the scatter plot
32 | y: []const f32,
33 | /// The style of the scatter plot
34 | style: Style = .{},
35 |
36 | /// Returns the range of the x values of the line plot
37 | fn getXRange(impl: *const anyopaque) Range(f32) {
38 | const self: *const Scatter = @ptrCast(@alignCast(impl));
39 | if (self.x) |x| {
40 | const min_max = std.mem.minMax(f32, x);
41 | return Range(f32) {
42 | .min = min_max.@"0",
43 | .max = min_max.@"1",
44 | };
45 | } else {
46 | return Range(f32) {
47 | .min = 0.0,
48 | .max = @floatFromInt(self.y.len - 1),
49 | };
50 | }
51 | }
52 |
53 | /// Returns the range of the y values of the line plot
54 | fn getYRange(impl: *const anyopaque) Range(f32) {
55 | const self: *const Scatter = @ptrCast(@alignCast(impl));
56 | const min_max = std.mem.minMax(f32, self.y);
57 | return Range(f32) {
58 | .min = min_max.@"0",
59 | .max = min_max.@"1",
60 | };
61 | }
62 |
63 | /// Draw the scatter plot (converts to SVG).
64 | fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) !void {
65 | const self: *const Scatter = @ptrCast(@alignCast(impl));
66 |
67 | if (self.x) |x_| {
68 | for(x_, self.y) |x, y| {
69 | if (!info.x_range.contains(x)) continue;
70 | if (!info.y_range.contains(y)) continue;
71 |
72 | const x1 = info.computeX(x);
73 | const y1 = info.computeY(y);
74 |
75 | try self.style.shape.writeTo(allocator, svg, x1, y1, self.style.radius, self.style.color);
76 | }
77 | } else {
78 | for (self.y, 0..) |y, x| {
79 | if (!info.x_range.contains(@floatFromInt(x))) continue;
80 | if (!info.y_range.contains(y)) continue;
81 |
82 | const x1 = info.computeX(@floatFromInt(x));
83 | const y1 = info.computeY(y);
84 |
85 | try self.style.shape.writeTo(allocator, svg, x1, y1, self.style.radius, self.style.color);
86 | }
87 | }
88 | }
89 |
90 | /// Converts the Scatter Plot to a Plot (its interface)
91 | pub fn interface(self: *const Scatter) Plot {
92 | return Plot.init(
93 | @as(*const anyopaque, self),
94 | self.style.title,
95 | self.style.color,
96 | &getXRange,
97 | &getYRange,
98 | &draw
99 | );
100 | }
--------------------------------------------------------------------------------
/src/plot/ShapeMarker.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const SVG = @import("../svg/SVG.zig");
5 | const RGB = @import("../svg/util/rgb.zig").RGB;
6 | const FigureInfo = @import("FigureInfo.zig");
7 | const Shape = @import("../util/shape.zig").Shape;
8 | const Marker = @import("Marker.zig");
9 |
10 | ////////////////////////////////////////////////////////////////////////////////////////////////////
11 | // Shape Marker //
12 | ////////////////////////////////////////////////////////////////////////////////////////////////////
13 |
14 | const ShapeMarker = @This();
15 |
16 | /// The x-axis value of the marker
17 | x: f32,
18 |
19 | /// The y-axis value of the marker
20 | y: f32,
21 |
22 | /// The shape of the marker
23 | shape: Shape = Shape.cross,
24 |
25 | /// The size of the marker
26 | size: f32 = 8.0,
27 |
28 | /// The color of the marker
29 | color: RGB = 0x000000,
30 |
31 | /// The label of the marker (null = no label)
32 | label: ?[]const u8 = null,
33 |
34 | /// The color of the label (null = same as the marker)
35 | label_color: ?RGB = null,
36 |
37 | /// The size of the label
38 | label_size: f32 = 12.0,
39 |
40 | /// The weight of the label
41 | label_weight: SVG.Text.FontWeight = .normal,
42 |
43 | /// Draws the marker
44 | pub fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) anyerror!void {
45 | const self: *const ShapeMarker = @ptrCast(@alignCast(impl));
46 |
47 | const x = info.computeX(self.x);
48 | const y = info.computeY(self.y);
49 |
50 | try self.shape.writeTo(allocator, svg, x, y, self.size, self.color);
51 |
52 | if (self.label) |label| {
53 | const label_x = x + self.size + 8.0;
54 | const label_y = y + self.size / 2.0;
55 | try svg.addText(.{
56 | .text = label,
57 | .x = .{ .pixel = label_x },
58 | .y = .{ .pixel = label_y },
59 | .font_size = .{ .pixel = self.label_size },
60 | .fill = self.label_color orelse self.color,
61 | .font_weight = self.label_weight,
62 | });
63 | }
64 | }
65 |
66 | /// Convert the ShapeMarker to a Marker
67 | pub fn interface(self: *const ShapeMarker) Marker {
68 | return Marker.init(
69 | @as(*const anyopaque, self),
70 | &ShapeMarker.draw,
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/plot/Stem.zig:
--------------------------------------------------------------------------------
1 | //! The Stem plot
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | const SVG = @import("../svg/SVG.zig");
7 | const RGB = @import("../svg/util/rgb.zig").RGB;
8 | const Range = @import("../util/range.zig").Range;
9 |
10 | const Plot = @import("Plot.zig");
11 | const FigureInfo = @import("FigureInfo.zig");
12 |
13 | const Shape = @import("../util/shape.zig").Shape;
14 |
15 | const Stem = @This();
16 |
17 | /// The style of the stem plot
18 | pub const Style = struct {
19 | /// The title of the plot
20 | title: ?[]const u8 = null,
21 | /// The color of the line
22 | color: RGB = 0x0000FF,
23 | /// The width of the line
24 | width: f32 = 2.0,
25 | /// The shape of the end of the stem
26 | shape: Shape = .circle,
27 | /// The radius of the shape at the end of the stem
28 | radius: f32 = 4.0,
29 | };
30 |
31 | /// The x-axis values of the stem plot
32 | x: ?[]const f32 = null,
33 | /// The y-axis values of the stem plot
34 | y: []const f32,
35 | /// The style of the stem plot
36 | style: Style = .{},
37 |
38 | /// Returns the range of the x values of the stem plot
39 | pub fn getXRange(impl: *const anyopaque) Range(f32) {
40 | const self: *const Stem = @ptrCast(@alignCast(impl));
41 | if (self.x) |x| {
42 | const min_max = std.mem.minMax(f32, x);
43 | return Range(f32) {
44 | .min = min_max.@"0",
45 | .max = min_max.@"1",
46 | };
47 | } else {
48 | return Range(f32) {
49 | .min = 0.0,
50 | .max = @floatFromInt(self.y.len - 1),
51 | };
52 | }
53 | }
54 |
55 | /// Returns the range of the y values of the stem plot
56 | pub fn getYRange(impl: *const anyopaque) Range(f32) {
57 | const self: *const Stem = @ptrCast(@alignCast(impl));
58 | const min_max = std.mem.minMax(f32, self.y);
59 | return Range(f32) {
60 | .min = min_max.@"0",
61 | .max = min_max.@"1",
62 | };
63 | }
64 |
65 | /// Draws the stem plot (converts to SVG)
66 | pub fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) !void {
67 | const self: *const Stem = @ptrCast(@alignCast(impl));
68 |
69 | const y_base = info.getBaseY();
70 | if (self.x) |x_| {
71 | for(x_, self.y) |x, y| {
72 | if (!info.x_range.contains(x)) continue;
73 | if (!info.y_range.contains(y)) continue;
74 |
75 | const y1 = info.computeY(y);
76 | const x1 = info.computeX(x);
77 |
78 | try svg.addLine(
79 | .{
80 | .x1 = .{ .pixel = x1 },
81 | .y1 = .{ .pixel = y_base },
82 | .x2 = .{ .pixel = x1 },
83 | .y2 = . { .pixel = y1 },
84 | .stroke = self.style.color,
85 | .stroke_width = . { .pixel = self.style.width },
86 | },
87 | );
88 |
89 | try self.style.shape.writeTo(allocator, svg, x1, y1, self.style.radius, self.style.color);
90 | }
91 | } else {
92 | for (self.y, 0..) |y, x| {
93 | if (!info.x_range.contains(@floatFromInt(x))) continue;
94 | if (!info.y_range.contains(y)) continue;
95 |
96 | const y1 = info.computeY(y);
97 | const x1 = info.computeX(@floatFromInt(x));
98 |
99 | try svg.addLine(
100 | .{
101 | .x1 = .{ .pixel = x1 },
102 | .y1 = .{ .pixel = y_base },
103 | .x2 = .{ .pixel = x1 },
104 | .y2 = . { .pixel = y1 },
105 | .stroke = self.style.color,
106 | .stroke_width = . { .pixel = self.style.width },
107 | },
108 | );
109 |
110 | try self.style.shape.writeTo(allocator, svg, x1, y1, self.style.radius, self.style.color);
111 | }
112 | }
113 | }
114 |
115 | /// Convert the Stem Plot to a Plot (its interface)
116 | pub fn interface(self: *const Stem) Plot {
117 | return Plot.init(
118 | @as(*const anyopaque, self),
119 | self.style.title,
120 | self.style.color,
121 | &getXRange,
122 | &getYRange,
123 | &draw
124 | );
125 | }
--------------------------------------------------------------------------------
/src/plot/Step.zig:
--------------------------------------------------------------------------------
1 | //! The Step plot
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | const SVG = @import("../svg/SVG.zig");
7 | const RGB = @import("../svg/util/rgb.zig").RGB;
8 | const Range = @import("../util/range.zig").Range;
9 |
10 | const Plot = @import("Plot.zig");
11 | const FigureInfo = @import("FigureInfo.zig");
12 |
13 | const Step = @This();
14 |
15 | /// The style of the step plot
16 | pub const Style = struct {
17 | /// The title of the plot
18 | title: ?[]const u8 = null,
19 | /// The color of the line
20 | color: RGB = 0x0000FF,
21 | /// The width of the line
22 | width: f32 = 2.0,
23 | };
24 |
25 | /// The x-axis values of the step plot
26 | x: ?[]const f32 = null,
27 | /// The y-axis values of the step plot
28 | y: []const f32,
29 | /// The style of the step plot
30 | style: Style = .{},
31 |
32 | /// Returns the range of the x values of the step plot
33 | fn getXRange(impl: *const anyopaque) Range(f32) {
34 | const self: *const Step = @ptrCast(@alignCast(impl));
35 | if (self.x) |x| {
36 | const min_max = std.mem.minMax(f32, x);
37 | return Range(f32) {
38 | .min = min_max.@"0",
39 | .max = min_max.@"1",
40 | };
41 | } else {
42 | return Range(f32) {
43 | .min = 0.0,
44 | .max = @floatFromInt(self.y.len - 1),
45 | };
46 | }
47 | }
48 |
49 | /// Returns the range of the y values of the step plot
50 | fn getYRange(impl: *const anyopaque) Range(f32) {
51 | const self: *const Step = @ptrCast(@alignCast(impl));
52 | const min_max = std.mem.minMax(f32, self.y);
53 | return Range(f32) {
54 | .min = min_max.@"0",
55 | .max = min_max.@"1",
56 | };
57 | }
58 |
59 | /// Draws the step plot (converts to SVG)
60 | fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) !void {
61 | const self: *const Step = @ptrCast(@alignCast(impl));
62 | _ = allocator;
63 |
64 | if (self.x) |x_| {
65 | var previous: ?f32 = null;
66 | var previous_x: ?f32 = null;
67 | for(x_, self.y) |x, y| {
68 | if (!info.x_range.contains(x)) continue;
69 | if (!info.y_range.contains(y)) continue;
70 |
71 | if (previous == null) {
72 | previous = y;
73 | previous_x = x;
74 | continue; // Skipping the 1st iteration
75 | }
76 |
77 | if (y != previous.?) {
78 | const x1 = info.computeX(previous_x.?);
79 | const y1 = info.computeY(previous.?);
80 | const y2 = info.computeY(y);
81 |
82 | try svg.addLine(.{
83 | .x1 = .{ .pixel = x1 },
84 | .y1 = .{ .pixel = y1 },
85 | .x2 = .{ .pixel = x1 },
86 | .y2 = .{ .pixel = y2 },
87 | .stroke = self.style.color,
88 | .stroke_width = .{ .pixel = self.style.width },
89 | });
90 | }
91 |
92 | const x1 = info.computeX(previous_x.?);
93 | const y1 = info.computeY(y);
94 | const x2 = info.computeX(x);
95 |
96 | try svg.addLine(
97 | .{
98 | .x1 = .{ .pixel = x1 },
99 | .y1 = .{ .pixel = y1 },
100 | .x2 = .{ .pixel = x2 },
101 | .y2 = . { .pixel = y1 },
102 | .stroke = self.style.color,
103 | .stroke_width = . { .pixel = self.style.width },
104 | },
105 | );
106 |
107 | previous = y;
108 | previous_x = x;
109 | }
110 | } else {
111 | var previous: ?f32 = null;
112 | var previous_x: ?f32 = null;
113 | for (self.y, 0..) |y, x| {
114 | if (!info.x_range.contains(@floatFromInt(x))) continue;
115 | if (!info.y_range.contains(y)) continue;
116 |
117 | if (previous == null) {
118 | previous = y;
119 | previous_x = @floatFromInt(x);
120 | continue; // Skipping the 1st iteration
121 | }
122 |
123 | if (y != previous.?) {
124 | const x1 = info.computeX(previous_x.?);
125 | const y1 = info.computeY(previous.?);
126 | const y2 = info.computeY(y);
127 |
128 | try svg.addLine(.{
129 | .x1 = .{ .pixel = x1 },
130 | .y1 = .{ .pixel = y1 },
131 | .x2 = .{ .pixel = x1 },
132 | .y2 = .{ .pixel = y2 },
133 | .stroke = self.style.color,
134 | .stroke_width = .{ .pixel = self.style.width },
135 | });
136 | }
137 |
138 | const x1 = info.computeX(previous_x.?);
139 | const y1 = info.computeY(y);
140 | const x2 = info.computeX(@floatFromInt(x));
141 |
142 | try svg.addLine(
143 | .{
144 | .x1 = .{ .pixel = x1 },
145 | .y1 = .{ .pixel = y1 },
146 | .x2 = .{ .pixel = x2 },
147 | .y2 = . { .pixel = y1 },
148 | .stroke = self.style.color,
149 | .stroke_width = . { .pixel = self.style.width },
150 | },
151 | );
152 |
153 | previous = y;
154 | previous_x = @floatFromInt(x);
155 | }
156 | }
157 | }
158 |
159 | /// Convert the Step Plot to a Plot (its interface)
160 | pub fn interface(self: *const Step) Plot {
161 | return Plot.init(
162 | @as(*const anyopaque, self),
163 | self.style.title,
164 | self.style.color,
165 | &getXRange,
166 | &getYRange,
167 | &draw
168 | );
169 | }
--------------------------------------------------------------------------------
/src/plot/TextMarker.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const SVG = @import("../svg/SVG.zig");
5 | const RGB = @import("../svg/util/rgb.zig").RGB;
6 | const FigureInfo = @import("FigureInfo.zig");
7 | const Marker = @import("Marker.zig");
8 |
9 | ////////////////////////////////////////////////////////////////////////////////////////////////////
10 | // Text Marker //
11 | ////////////////////////////////////////////////////////////////////////////////////////////////////
12 |
13 | const TextMarker = @This();
14 |
15 | /// The x-axis value of the marker
16 | x: f32,
17 |
18 | /// The y-axis value of the marker
19 | y: f32,
20 |
21 | /// The color of the marker
22 | color: RGB = 0x000000,
23 |
24 | /// The text of the marker
25 | text: []const u8,
26 |
27 | /// The size of the text
28 | size: f32 = 12.0,
29 |
30 | /// The weight of the text
31 | weight: SVG.Text.FontWeight = .normal,
32 |
33 | /// Draws the marker
34 | pub fn draw(impl: *const anyopaque, allocator: Allocator, svg: *SVG, info: FigureInfo) anyerror!void {
35 | _ = allocator;
36 | const self: *const TextMarker = @ptrCast(@alignCast(impl));
37 |
38 | const x = info.computeX(self.x);
39 | const y = info.computeY(self.y);
40 |
41 | try svg.addText(.{
42 | .text = self.text,
43 | .x = .{ .pixel = x },
44 | .y = .{ .pixel = y },
45 | .font_size = .{ .pixel = self.size },
46 | .fill = self.color,
47 | .font_weight = self.weight,
48 | });
49 | }
50 |
51 | /// Convert the ShapeMarker to a Marker
52 | pub fn interface(self: *const TextMarker) Marker {
53 | return Marker.init(
54 | @as(*const anyopaque, self),
55 | &TextMarker.draw,
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/plot/formatters.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | /// The default formatter that prints the value with 2 decimal places (precision can be changed)
4 | pub fn default(
5 | options: struct {
6 | comptime precision: ?u8 = 2,
7 | },
8 | ) *const fn (*std.ArrayList(u8), f32) anyerror!void {
9 | if (options.precision == null) {
10 | return &struct {
11 | pub fn lambda(buffer: *std.ArrayList(u8), value: f32) anyerror!void {
12 | try buffer.writer().print("{d}", .{value});
13 | }
14 | }.lambda;
15 | } else {
16 | const precision_str = std.fmt.comptimePrint("{}", .{options.precision.?});
17 |
18 | return &struct {
19 | pub fn lambda(buffer: *std.ArrayList(u8), value: f32) anyerror!void {
20 | try buffer.writer().print("{d:." ++ precision_str ++ "}", .{value});
21 | }
22 | }.lambda;
23 | }
24 | }
25 |
26 | /// The default formatter that prints the value with 2 decimal places (precision can be changed)
27 | pub fn scientific(
28 | options: struct {
29 | comptime precision: ?u8 = 2,
30 | },
31 | ) *const fn (*std.ArrayList(u8), f32) anyerror!void {
32 | if (options.precision == null) {
33 | return &struct {
34 | pub fn lambda(buffer: *std.ArrayList(u8), value: f32) anyerror!void {
35 | try buffer.writer().print("{}", .{value});
36 | }
37 | }.lambda;
38 | } else {
39 | const precision_str = std.fmt.comptimePrint("{}", .{options.precision.?});
40 |
41 | return &struct {
42 | pub fn lambda(buffer: *std.ArrayList(u8), value: f32) anyerror!void {
43 | try buffer.writer().print("{:." ++ precision_str ++ "}", .{value});
44 | }
45 | }.lambda;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/root.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | // Figure (plot module)
4 | pub const Figure = @import("plot/Figure.zig");
5 | pub const FigureInfo = @import("plot/FigureInfo.zig");
6 |
7 | // Plots (plot module)
8 | pub const Plot = @import("plot/Plot.zig");
9 | pub const Line = @import("plot/Line.zig");
10 | pub const Area = @import("plot/Area.zig");
11 | pub const Scatter = @import("plot/Scatter.zig");
12 | pub const Step = @import("plot/Step.zig");
13 | pub const Stem = @import("plot/Stem.zig");
14 | pub const CandleStick = @import("plot/CandleStick.zig");
15 |
16 | // Markers (plot module)
17 | pub const Marker = @import("plot/Marker.zig");
18 | pub const ShapeMarker = @import("plot/ShapeMarker.zig");
19 | pub const TextMarker = @import("plot/TextMarker.zig");
20 |
21 | // Util Module
22 | pub const Range = @import("util/range.zig").Range;
23 | pub const polyshape = @import("util/polyshape.zig");
24 |
25 | // SVG Module
26 | const SVG = @import("svg/SVG.zig");
27 | const length = @import("svg/util/length.zig");
28 | const LengthPercent = length.LengthPercent;
29 | const LengthPercentAuto = length.LengthPercentAuto;
30 | pub const rgb = @import("svg/util/rgb.zig");
31 | pub const RGB = rgb.RGB;
32 |
33 | test "Plot Test" {
34 | std.testing.refAllDecls(FigureInfo);
35 | std.testing.refAllDecls(Figure);
36 | }
37 |
--------------------------------------------------------------------------------
/src/svg/Circle.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Kind = @import("kind.zig").Kind;
4 |
5 | const length = @import("util/length.zig");
6 | const LengthPercent = length.LengthPercent;
7 | const LenghtPercentAuto = length.LengthPercentAuto;
8 |
9 | const rgb = @import("util/rgb.zig");
10 | const RGB = rgb.RGB;
11 |
12 | const Circle = @This();
13 |
14 | /// The options of the Circle.
15 | pub const Options = struct {
16 | /// The x coordinate of the center of the circle
17 | center_x: LengthPercent = .{ .pixel = 0.0 },
18 | /// The y coordinate of the center of the circle
19 | center_y: LengthPercent = .{ .pixel = 0.0 },
20 | /// The radius of the circle
21 | radius: LengthPercent = .{ .pixel = 0.0 },
22 | /// The color of the fill of the circle
23 | fill: ?RGB = null,
24 | /// The opacity of the fill of the circle
25 | fill_opacity: f32 = 1.0,
26 | /// The color of the stroke of the circle
27 | stroke: ?RGB = null,
28 | /// The opacity of the stroke of the circle
29 | stroke_opacity: f32 = 1.0,
30 | /// The width of the stroke of the circle
31 | stroke_width: LengthPercent = .{ .pixel = 0.0 },
32 | /// The opacity of the circle
33 | opacity: f32 = 1.0,
34 | };
35 |
36 | /// The options of the circle
37 | options: Options,
38 |
39 | /// Initialize a circle with the given options
40 | pub fn init(options: Options) Circle {
41 | return Circle {
42 | .options = options,
43 | };
44 | }
45 |
46 | /// Write the circle to the given writer
47 | pub fn writeTo(self: *const Circle, writer: anytype) anyerror!void {
48 | try writer.writeAll("6}\" ", .{fill})
53 | else try writer.writeAll("fill=\"none\" ");
54 | try writer.print("fill-opacity=\"{d}\" ", .{self.options.fill_opacity});
55 | if (self.options.stroke) |stroke| try writer.print("stroke=\"#{X:0>6}\" ", .{stroke})
56 | else try writer.writeAll("stroke=\"none\" ");
57 | try writer.print("stroke-opacity=\"{d}\" ", .{self.options.stroke_opacity});
58 | try writer.print("stroke-width=\"{}\" ", .{self.options.stroke_width});
59 | try writer.print("opacity=\"{d}\" ", .{self.options.opacity});
60 | try writer.writeAll("/>");
61 | }
62 |
63 | /// Wrap the circle into a kind
64 | pub fn wrap(self: *const Circle) Kind {
65 | return Kind {
66 | .circle = self.*
67 | };
68 | }
--------------------------------------------------------------------------------
/src/svg/Line.zig:
--------------------------------------------------------------------------------
1 | //! A SVG Line component
2 |
3 | const std = @import("std");
4 |
5 | const Kind = @import("kind.zig").Kind;
6 |
7 | const length = @import("util/length.zig");
8 | const LengthPercent = length.LengthPercent;
9 |
10 | const rgb = @import("util/rgb.zig");
11 | const RGB = rgb.RGB;
12 |
13 | const Line = @This();
14 |
15 | /// The line cap options
16 | pub const LineCap = enum {
17 | butt,
18 | round,
19 | square,
20 |
21 | pub fn format(self: LineCap, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
22 | _ = fmt;
23 | _ = options;
24 | switch (self) {
25 | .butt => try writer.writeAll("butt"),
26 | .round => try writer.writeAll("round"),
27 | .square => try writer.writeAll("square"),
28 | }
29 | }
30 | };
31 |
32 | /// The options of the Line
33 | pub const Options = struct {
34 | /// The starting x-coordinate of the line
35 | x1: LengthPercent = .{ .pixel = 0.0 },
36 | /// The starting y-coordinate of the line
37 | y1: LengthPercent = .{ .pixel = 0.0 },
38 | /// The ending x-coordinate of the line
39 | x2: LengthPercent = .{ .pixel = 1.0 },
40 | /// The ending y-coordinate of the line
41 | y2: LengthPercent = .{ .pixel = 1.0 },
42 | /// The color of the stroke of the line
43 | stroke: ?RGB = null,
44 | /// opacity of the stroke
45 | stroke_opacity: f32 = 1.0,
46 | /// The width of the stroke
47 | stroke_width: LengthPercent = .{ .pixel = 1.0 },
48 | /// The line cap of the stroke
49 | stroke_linecap: LineCap = .butt,
50 | /// The dash array of the stroke
51 | stroke_dasharray: ?[]const f32 = null,
52 | /// The opacity of the line
53 | opacity: f32 = 1.0,
54 | };
55 |
56 | /// The options of the Line
57 | options: Options,
58 |
59 | /// Initialize the Line with the given options
60 | pub fn init(options: Options) Line {
61 | return Line {
62 | .options = options,
63 | };
64 | }
65 |
66 | /// Write the line to the given writer
67 | pub fn writeTo(self: *const Line, writer: anytype) anyerror!void {
68 | try writer.writeAll("6}\" ", .{stroke})
74 | else try writer.writeAll("stroke=\"none\" ");
75 | try writer.print("stroke-opacity=\"{d}\" ", .{self.options.stroke_opacity});
76 | try writer.print("stroke-width=\"{}\" ", .{self.options.stroke_width});
77 | try writer.print("stroke-linecap=\"{}\" ", .{self.options.stroke_linecap});
78 | if (self.options.stroke_dasharray) |stroke_dash_array| {
79 | try writer.writeAll("stroke-dasharray=\" ");
80 | for (stroke_dash_array) |dash| {
81 | try writer.print("{} ", .{dash});
82 | }
83 | try writer.writeAll("\" ");
84 | }
85 | try writer.print("opacity=\"{d}\" ", .{self.options.opacity});
86 | try writer.writeAll("/>");
87 | }
88 |
89 | /// Wrap the line into a kind
90 | pub fn wrap(self: *const Line) Kind {
91 | return Kind {
92 | .line = self.*
93 | };
94 | }
--------------------------------------------------------------------------------
/src/svg/Path.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | pub const Kind = @import("kind.zig").Kind;
5 |
6 | const rgb = @import("util/rgb.zig");
7 | const RGB = rgb.RGB;
8 |
9 | const length = @import("util/length.zig");
10 | const LengthPercent = length.LengthPercent;
11 |
12 | const Path = @This();
13 |
14 | /// The command for the path
15 | pub const Command = union(enum) {
16 | /// `M x y`
17 | MoveTo: struct {
18 | x: f32,
19 | y: f32,
20 | },
21 | /// `m dx dy`
22 | MoveToRelative: struct {
23 | x: f32,
24 | y: f32,
25 | },
26 | /// `L x y`
27 | LineTo: struct {
28 | x: f32,
29 | y: f32,
30 | },
31 | /// `l dx dy`
32 | LineToRelative: struct {
33 | x: f32,
34 | y: f32,
35 | },
36 | /// `H x`
37 | HorizontalLineTo: struct {
38 | x: f32,
39 | },
40 | /// `h dx`
41 | HorizontalLineToRelative: struct {
42 | x: f32,
43 | },
44 | /// `V y`
45 | VerticalLineTo: struct {
46 | y: f32,
47 | },
48 | /// `v dy`
49 | VerticalLineToRelative: struct {
50 | y: f32,
51 | },
52 | /// `C x1 y1 x2 y2 x y`
53 | CubicBezierCurveTo: struct {
54 | x1: f32,
55 | y1: f32,
56 | x2: f32,
57 | y2: f32,
58 | x: f32,
59 | y: f32,
60 | },
61 | /// `c dx1 dy1 dx2 dy2 dx dy`
62 | CubicBezierCurveToRelative: struct {
63 | dx1: f32,
64 | dy1: f32,
65 | dx2: f32,
66 | dy2: f32,
67 | dx: f32,
68 | dy: f32,
69 | },
70 | /// `S x2 y2 x y`
71 | SmoothCubicBezierCurveTo: struct {
72 | x2: f32,
73 | y2: f32,
74 | x: f32,
75 | y: f32,
76 | },
77 | /// `s dx2 dy2 dx dy`
78 | SmoothCubicBezierCurveToRelative: struct {
79 | dx2: f32,
80 | dy2: f32,
81 | dx: f32,
82 | dy: f32,
83 | },
84 | /// `Q x1 y1 x y`
85 | QuadraticBezierCurveTo: struct {
86 | x1: f32,
87 | y1: f32,
88 | x: f32,
89 | y: f32,
90 | },
91 | /// `q dx1 dy1 dx dy`
92 | QuadraticBezierCurveToRelative: struct {
93 | dx1: f32,
94 | dy1: f32,
95 | dx: f32,
96 | dy: f32,
97 | },
98 | /// `T x y`
99 | SmoothQuadraticBezierCurveTo: struct {
100 | x: f32,
101 | y: f32,
102 | },
103 | /// `t dx dy`
104 | SmoothQuadraticBezierCurveToRelative: struct {
105 | dx: f32,
106 | dy: f32,
107 | },
108 | /// `A rx ry x-axis-rotation large-arc-flag sweep-flag x y`
109 | EllipticalArcTo: struct {
110 | rx: f32,
111 | ry: f32,
112 | x_axis_rotation: f32,
113 | large_arc_flag: bool,
114 | sweep_flag: bool,
115 | x: f32,
116 | y: f32,
117 | },
118 | /// `a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy`
119 | EllipticalArcToRelative: struct {
120 | rx: f32,
121 | ry: f32,
122 | x_axis_rotation: f32,
123 | large_arc_flag: bool,
124 | sweep_flag: bool,
125 | dx: f32,
126 | dy: f32,
127 | },
128 | /// `Z`
129 | ClosePath: void,
130 |
131 | /// Write the command to the given writer
132 | pub fn writeTo(self: *const Command, writer: anytype) anyerror!void {
133 | switch (self.*) {
134 | .MoveTo => {
135 | try writer.print("M {d} {d}", .{ self.MoveTo.x, self.MoveTo.y });
136 | },
137 | .MoveToRelative => {
138 | try writer.print("m {d} {d}", .{ self.MoveToRelative.x, self.MoveToRelative.y });
139 | },
140 | .LineTo => {
141 | try writer.print("L {d} {d}", .{ self.LineTo.x, self.LineTo.y });
142 | },
143 | .LineToRelative => {
144 | try writer.print("l {d} {d}", .{ self.LineToRelative.x, self.LineToRelative.y });
145 | },
146 | .HorizontalLineTo => {
147 | try writer.print("H {d}", .{self.HorizontalLineTo.x});
148 | },
149 | .HorizontalLineToRelative => {
150 | try writer.print("h {d}", .{self.HorizontalLineToRelative.x});
151 | },
152 | .VerticalLineTo => {
153 | try writer.print("V {d}", .{self.VerticalLineTo.y});
154 | },
155 | .VerticalLineToRelative => {
156 | try writer.print("v {d}", .{self.VerticalLineToRelative.y});
157 | },
158 | .CubicBezierCurveTo => {
159 | try writer.print("C {d} {d} {d} {d} {d} {d}", .{
160 | self.CubicBezierCurveTo.x1,
161 | self.CubicBezierCurveTo.y1,
162 | self.CubicBezierCurveTo.x2,
163 | self.CubicBezierCurveTo.y2,
164 | self.CubicBezierCurveTo.x,
165 | self.CubicBezierCurveTo.y,
166 | });
167 | },
168 | .CubicBezierCurveToRelative => {
169 | try writer.print("c {d} {d} {d} {d} {d} {d}", .{
170 | self.CubicBezierCurveToRelative.dx1,
171 | self.CubicBezierCurveToRelative.dy1,
172 | self.CubicBezierCurveToRelative.dx2,
173 | self.CubicBezierCurveToRelative.dy2,
174 | self.CubicBezierCurveToRelative.dx,
175 | self.CubicBezierCurveToRelative.dy,
176 | });
177 | },
178 | .SmoothCubicBezierCurveTo => {
179 | try writer.print("S {d} {d} {d} {d}", .{
180 | self.SmoothCubicBezierCurveTo.x2,
181 | self.SmoothCubicBezierCurveTo.y2,
182 | self.SmoothCubicBezierCurveTo.x,
183 | self.SmoothCubicBezierCurveTo.y,
184 | });
185 | },
186 | .SmoothCubicBezierCurveToRelative => {
187 | try writer.print("s {d} {d} {d} {d}", .{
188 | self.SmoothCubicBezierCurveToRelative.dx2,
189 | self.SmoothCubicBezierCurveToRelative.dy2,
190 | self.SmoothCubicBezierCurveToRelative.dx,
191 | self.SmoothCubicBezierCurveToRelative.dy,
192 | });
193 | },
194 | .QuadraticBezierCurveTo => {
195 | try writer.print("Q {d} {d} {d} {d}", .{
196 | self.QuadraticBezierCurveTo.x1,
197 | self.QuadraticBezierCurveTo.y1,
198 | self.QuadraticBezierCurveTo.x,
199 | self.QuadraticBezierCurveTo.y,
200 | });
201 | },
202 | .QuadraticBezierCurveToRelative => {
203 | try writer.print("q {d} {d} {d} {d}", .{
204 | self.QuadraticBezierCurveToRelative.dx1,
205 | self.QuadraticBezierCurveToRelative.dy1,
206 | self.QuadraticBezierCurveToRelative.dx,
207 | self.QuadraticBezierCurveToRelative.dy,
208 | });
209 | },
210 | .SmoothQuadraticBezierCurveTo => {
211 | try writer.print("T {d} {d}", .{ self.SmoothQuadraticBezierCurveTo.x, self.SmoothQuadraticBezierCurveTo.y });
212 | },
213 | .SmoothQuadraticBezierCurveToRelative => {
214 | try writer.print("t {d} {d}", .{ self.SmoothQuadraticBezierCurveToRelative.dx, self.SmoothQuadraticBezierCurveToRelative.dy });
215 | },
216 | .EllipticalArcTo => {
217 | try writer.print("A {d} {d} {d} {s} {s} {d} {d}", .{
218 | self.EllipticalArcTo.rx,
219 | self.EllipticalArcTo.ry,
220 | self.EllipticalArcTo.x_axis_rotation,
221 | if (self.EllipticalArcTo.large_arc_flag) "1" else "0",
222 | if (self.EllipticalArcTo.sweep_flag) "1" else "0",
223 | self.EllipticalArcTo.x,
224 | self.EllipticalArcTo.y,
225 | });
226 | },
227 | .EllipticalArcToRelative => {
228 | try writer.print("a {d} {d} {d} {s} {s} {d} {d}", .{
229 | self.EllipticalArcToRelative.rx,
230 | self.EllipticalArcToRelative.ry,
231 | self.EllipticalArcToRelative.x_axis_rotation,
232 | if (self.EllipticalArcToRelative.large_arc_flag) "1" else "0",
233 | if (self.EllipticalArcToRelative.sweep_flag) "1" else "0",
234 | self.EllipticalArcToRelative.dx,
235 | self.EllipticalArcToRelative.dy,
236 | });
237 | },
238 | .ClosePath => {
239 | try writer.writeAll("Z");
240 | },
241 | }
242 | }
243 | };
244 |
245 | pub const Options = struct {
246 | /// The commands for the path
247 | commands: ?[]Command = null,
248 | /// The allocator for the commands (null means not-allocated)
249 | allocator: ?Allocator = null,
250 | /// The color of the fill
251 | fill: ?RGB = null,
252 | /// The opacity of the fill
253 | fill_opacity: f32 = 1.0,
254 | /// The color of the stroke
255 | stroke: ?RGB = null,
256 | /// The opacity of the stroke
257 | stroke_opacity: f32 = 1.0,
258 | /// The width of the stroke
259 | stroke_width: LengthPercent = .{ .pixel = 1.0 },
260 | /// The dash array of the stroke
261 | stroke_dasharray: ?[]const f32 = null,
262 | /// The opacity of the stroke
263 | opacity: f32 = 1.0,
264 | };
265 |
266 | /// The options of the path
267 | options: Options,
268 |
269 | /// Initialize the path with the given options
270 | pub fn init(options: Options) Path {
271 | return Path{
272 | .options = options,
273 | };
274 | }
275 |
276 | /// Deinitialize the path
277 | pub fn deinit(self: *const Path) void {
278 | if (self.options.allocator) |allocator| {
279 | if (self.options.commands) |commands| {
280 | allocator.free(commands);
281 | }
282 | }
283 | }
284 |
285 | /// Write the path to the given writer
286 | pub fn writeTo(self: *const Path, writer: anytype) anyerror!void {
287 | try writer.writeAll("6}\" ", .{fill}) else try writer.writeAll("fill=\"none\" ");
297 | try writer.print("fill-opacity=\"{d}\" ", .{self.options.fill_opacity});
298 | if (self.options.stroke) |stroke| try writer.print("stroke=\"#{X:0>6}\" ", .{stroke}) else try writer.writeAll("stroke=\"none\" ");
299 | try writer.print("stroke-opacity=\"{d}\" ", .{self.options.stroke_opacity});
300 | try writer.print("stroke-width=\"{}\" ", .{self.options.stroke_width});
301 | if (self.options.stroke_dasharray) |dasharray| {
302 | try writer.writeAll("stroke-dasharray=\"");
303 | for (dasharray) |dash| {
304 | try writer.print("{d} ", .{dash});
305 | }
306 | try writer.writeAll("\" ");
307 | }
308 | try writer.print("opacity=\"{d}\" ", .{self.options.opacity});
309 | try writer.writeAll("/>");
310 | }
311 |
312 | pub fn wrap(self: *const Path) Kind {
313 | return Kind{ .path = self.* };
314 | }
315 |
--------------------------------------------------------------------------------
/src/svg/Polyline.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const Kind = @import("kind.zig").Kind;
5 |
6 | const length = @import("util/length.zig");
7 | const LengthPercent = length.LengthPercent;
8 |
9 | const rgb = @import("util/rgb.zig");
10 | const RGB = rgb.RGB;
11 |
12 | const Polyline = @This();
13 |
14 | /// The options of a Polyline
15 | pub const Options = struct {
16 | /// The points of the polyline
17 | points: ?[]const f32 = null,
18 | /// The allocator for the points (null means not-allocated)
19 | allocator: ?Allocator = null,
20 | /// The color of the fill
21 | fill: ?RGB = null,
22 | /// The opacity of the fill
23 | fill_opacity: f32 = 1.0,
24 | /// The color of the stroke
25 | stroke: ?RGB = null,
26 | /// The opacity of the stroke
27 | stroke_opacity: f32 = 1.0,
28 | /// The width of the stroke
29 | stroke_width: LengthPercent = .{ .pixel = 1.0 },
30 | /// The opacity of the Polyline (fill + stroke)
31 | opacity: f32 = 1.0,
32 | };
33 |
34 | /// The options of the polyline
35 | options: Options,
36 |
37 | /// Intialize the polyline with the given option
38 | pub fn init(options: Options) Polyline {
39 | return Polyline{
40 | .options = options,
41 | };
42 | }
43 |
44 | /// Deinitialize the polyline.
45 | pub fn deinit(self: *const Polyline) void {
46 | if (self.options.allocator) |allocator| {
47 | if (self.options.points) |points| {
48 | allocator.free(points);
49 | }
50 | }
51 | }
52 |
53 | /// Write the Polyline to the given writer.
54 | pub fn writeTo(self: *const Polyline, writer: anytype) anyerror!void {
55 | try writer.writeAll("6}\" ", .{fill}) else try writer.writeAll("fill=\"none\" ");
64 | try writer.print("fill-opacity=\"{d}\" ", .{self.options.fill_opacity});
65 | if (self.options.stroke) |stroke| try writer.print("stroke=\"#{X:0>6}\" ", .{stroke}) else try writer.writeAll("stroke=\"none\" ");
66 | try writer.print("stroke-opacity=\"{d}\" ", .{self.options.stroke_opacity});
67 | try writer.print("stroke-width=\"{}\" ", .{self.options.stroke_width});
68 | try writer.print("opacity=\"{d}\" ", .{self.options.opacity});
69 | try writer.writeAll("/>");
70 | }
71 |
72 | /// Wrap the Polyline in a Kind
73 | pub fn wrap(self: *const Polyline) Kind {
74 | return Kind{ .polyline = self.* };
75 | }
76 |
--------------------------------------------------------------------------------
/src/svg/Rect.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Kind = @import("kind.zig").Kind;
4 |
5 | const length = @import("util/length.zig");
6 | const LengthPercent = length.LengthPercent;
7 | const LengthPercentAuto = length.LengthPercentAuto;
8 |
9 | const rgb = @import("util/rgb.zig");
10 | const RGB = rgb.RGB;
11 |
12 | const Rect = @This();
13 |
14 | /// The options of the Rect
15 | pub const Options = struct {
16 | /// The x coordinate of the top left corner of the rectangle
17 | x: LengthPercentAuto = .{ .pixel = 0.0 },
18 | /// The y coordinate of the top left corner of the rectangle
19 | y: LengthPercentAuto = .{ .pixel = 0.0 },
20 | /// The width of the rectangle
21 | width: LengthPercentAuto = .{ .percent = 1.0 },
22 | /// The height of the rectangle
23 | height: LengthPercentAuto = .{ .percent = 1.0 },
24 | /// The x radius of the corner of the rectangle
25 | radius_x: LengthPercentAuto = .auto,
26 | /// The y radius of the corner of the rectangle
27 | radius_y: LengthPercentAuto = .auto,
28 | /// The color of the fill of the rectangle
29 | fill: ?RGB = null,
30 | /// The opacity of the fill of the rectangle
31 | fill_opacity: f32 = 1.0,
32 | /// The color of the stroke of the rectangle
33 | stroke: ?RGB = null,
34 | /// The opacity of the stroke of the rectangle
35 | stroke_opacity: f32 = 1.0,
36 | /// The width of the stroke of the rectangle
37 | stroke_width: LengthPercent = .{ .pixel = 1.0 },
38 | /// The opacity of the rectangle (stroke + fill)
39 | opacity: f32 = 1.0,
40 | };
41 |
42 | /// The options of the rectangle
43 | options: Options,
44 |
45 | /// Initialize the rectangle with the given options
46 | pub fn init(options: Options) Rect {
47 | return Rect {
48 | .options = options,
49 | };
50 | }
51 |
52 | /// Write the rectangle to the given writer
53 | pub fn writeTo(self: *const Rect, writer: anytype) anyerror!void {
54 | try writer.writeAll("6}\" ", .{fill})
62 | else try writer.writeAll("fill=\"none\" ");
63 | try writer.print("fill-opacity=\"{}\" ", .{self.options.fill_opacity});
64 | if (self.options.stroke) |stroke| try writer.print("stroke=\"#{X:0>6}\" ", .{stroke})
65 | else try writer.writeAll("stroke=\"none\" ");
66 | try writer.print("stroke-opacity=\"{}\" ", .{self.options.stroke_opacity});
67 | try writer.print("stroke-width=\"{}\" ", .{self.options.stroke_width});
68 | try writer.print("opacity=\"{}\" ", .{self.options.opacity});
69 | try writer.writeAll("/>");
70 | }
71 |
72 | /// Wrap the rectangle in a kind
73 | pub fn wrap(self: *const Rect) Kind {
74 | return Kind {
75 | .rect = self.*,
76 | };
77 | }
--------------------------------------------------------------------------------
/src/svg/SVG.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | pub const Line = @import("Line.zig");
5 | pub const Rect = @import("Rect.zig");
6 | pub const Circle = @import("Circle.zig");
7 | pub const Polyline = @import("Polyline.zig");
8 | pub const Text = @import("Text.zig");
9 | pub const Path = @import("Path.zig");
10 |
11 | pub usingnamespace @import("kind.zig");
12 |
13 | const SVG = @This();
14 |
15 | /// The repsentation of the viewbox of the SVG
16 | pub const ViewBox = struct { x: f32, y: f32, width: f32, height: f32 };
17 |
18 | /// The allocator used for the SVG
19 | allocator: Allocator,
20 | // The data inside the SVG (List of Kind)
21 | data: SVG.Kind.List,
22 |
23 | /// The width of the SVG
24 | width: f32,
25 | /// The height of the SVG
26 | height: f32,
27 | /// The viewbox of the SVG
28 | viewbox: ViewBox,
29 |
30 | /// Initialize the SVG with the given allocator, width and height
31 | pub fn init(allocator: Allocator, width: f32, height: f32) SVG {
32 | return SVG{ .allocator = allocator, .data = SVG.Kind.List.init(allocator), .width = width, .height = height, .viewbox = ViewBox{
33 | .x = 0,
34 | .y = 0,
35 | .width = width,
36 | .height = height,
37 | } };
38 | }
39 |
40 | /// Deintiialize the SVG
41 | pub fn deinit(self: *const SVG) void {
42 | for (self.data.items) |kind| {
43 | kind.deinit();
44 | }
45 | self.data.deinit();
46 | }
47 |
48 | /// Add a Kind to the SVG
49 | pub fn add(self: *SVG, kind: SVG.Kind) !void {
50 | try self.data.append(kind);
51 | }
52 |
53 | /// Add a Line to the SVG
54 | pub fn addLine(self: *SVG, options: Line.Options) !void {
55 | try self.add(Line.init(options).wrap());
56 | }
57 |
58 | /// Add a Rect to the SVG
59 | pub fn addRect(self: *SVG, options: Rect.Options) !void {
60 | try self.add(Rect.init(options).wrap());
61 | }
62 |
63 | /// Add a Circle to the SVG
64 | pub fn addCircle(self: *SVG, options: Circle.Options) !void {
65 | try self.add(Circle.init(options).wrap());
66 | }
67 |
68 | /// Add a Polyline to the SVG
69 | pub fn addPolyline(self: *SVG, options: Polyline.Options) !void {
70 | try self.add(Polyline.init(options).wrap());
71 | }
72 |
73 | /// Add a Text to the SVG
74 | pub fn addText(self: *SVG, options: Text.Options) !void {
75 | try self.add(Text.init(options).wrap());
76 | }
77 |
78 | /// Add a Path to the SVG
79 | pub fn addPath(self: *SVG, options: Path.Options) !void {
80 | try self.add(Path.init(options).wrap());
81 | }
82 |
83 | /// The header of the SVG
84 | const SVG_HEADER =
85 | \\
86 | \\
87 | \\");
109 | }
110 |
--------------------------------------------------------------------------------
/src/svg/Text.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const Kind = @import("kind.zig").Kind;
5 |
6 | const length = @import("util/length.zig");
7 | const LengthPercent = length.LengthPercent;
8 | const LengthPercentAuto = length.LengthPercentAuto;
9 |
10 | const rgb = @import("util/rgb.zig");
11 | const RGB = rgb.RGB;
12 |
13 | const Text = @This();
14 |
15 | /// Representation of the SVG FontSize property.
16 | pub const FontSize = union(enum) {
17 | /// The absolute size.
18 | pixel: f32,
19 | /// The relative size.
20 | em: f32,
21 | /// The size in percent of the parent
22 | percent: f32,
23 |
24 | /// Absolute Size - xx-small
25 | xx_small: void,
26 | /// Absolute Size - x-small
27 | x_small: void,
28 | /// Absolute Size - small
29 | small: void,
30 | /// Absolute Size - medium
31 | medium: void,
32 | /// Absolute Size - large
33 | large: void,
34 | /// Absolute Size - x-large
35 | x_large: void,
36 | /// Absolute Size - xx-large
37 | xx_large: void,
38 | /// Absolute Size - xxx-large
39 | xxx_large: void,
40 |
41 | /// Relateive Size - smaller
42 | smaller: void,
43 | /// Relateive Size - larger
44 | larger: void,
45 |
46 | /// Math value
47 | math: void,
48 |
49 | pub fn format(self: FontSize, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
50 | _ = fmt;
51 | _ = options;
52 | switch (self) {
53 | .pixel => |value| try writer.print("{d}px", .{value}),
54 | .em => |value| try writer.print("{d}em", .{value}),
55 | .percent => |value| try writer.print("{d}%", .{value}),
56 | .xx_small => try writer.writeAll("xx-small"),
57 | .x_small => try writer.writeAll("x-small"),
58 | .small => try writer.writeAll("small"),
59 | .medium => try writer.writeAll("medium"),
60 | .large => try writer.writeAll("large"),
61 | .x_large => try writer.writeAll("x-large"),
62 | .xx_large => try writer.writeAll("xx-large"),
63 | .xxx_large => try writer.writeAll("xxx-large"),
64 | .smaller => try writer.writeAll("smaller"),
65 | .larger => try writer.writeAll("larger"),
66 | .math => try writer.writeAll("math"),
67 | }
68 | }
69 | };
70 |
71 | /// Representation of the SVG FontWeight property
72 | pub const FontWeight = enum {
73 | /// The normal font weight
74 | normal,
75 | /// The bold font weight
76 | bold,
77 | /// The 100 font weight (thin)
78 | w100,
79 | /// The 200 font weight (extra light)
80 | w200,
81 | /// The 300 font weight (light)
82 | w300,
83 | /// The 400 font weight (normal)
84 | w400,
85 | /// The 500 font weight (medium)
86 | w500,
87 | /// The 600 font weight (semi bold)
88 | w600,
89 | /// The 700 font weight (bold)
90 | w700,
91 | /// The 800 font weight (extra bold)
92 | w800,
93 | /// The 900 font weight (black)
94 | w900,
95 | /// The lighter font weight
96 | lighter,
97 | /// The bolder font weight
98 | bolder,
99 |
100 | pub fn format(self: FontWeight, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
101 | _ = fmt;
102 | _ = options;
103 | switch (self) {
104 | .normal => try writer.writeAll("normal"),
105 | .bold => try writer.writeAll("bold"),
106 | .w100 => try writer.writeAll("100"),
107 | .w200 => try writer.writeAll("200"),
108 | .w300 => try writer.writeAll("300"),
109 | .w400 => try writer.writeAll("400"),
110 | .w500 => try writer.writeAll("500"),
111 | .w600 => try writer.writeAll("600"),
112 | .w700 => try writer.writeAll("700"),
113 | .w800 => try writer.writeAll("800"),
114 | .w900 => try writer.writeAll("900"),
115 | .lighter => try writer.writeAll("lighter"),
116 | .bolder => try writer.writeAll("bolder"),
117 | }
118 | }
119 | };
120 |
121 | /// Representation of the SVG TextAnchor property
122 | pub const TextAnchor = enum {
123 | /// The start anchor
124 | start,
125 | /// The middle anchor
126 | middle,
127 | /// The end anchor
128 | end,
129 |
130 | pub fn format(self: TextAnchor, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
131 | _ = fmt;
132 | _ = options;
133 | switch (self) {
134 | .start => try writer.writeAll("start"),
135 | .middle => try writer.writeAll("middle"),
136 | .end => try writer.writeAll("end"),
137 | }
138 | }
139 | };
140 |
141 | /// Representation of the SVG DominantBaseline property
142 | pub const DominantBaseline = enum {
143 | /// The auto baseline
144 | auto,
145 | /// The use script baseline
146 | use_script,
147 | /// The no change baseline
148 | no_change,
149 | /// The reset size baseline
150 | reset_size,
151 | /// The ideographic baseline
152 | ideographic,
153 | /// The alphabetic baseline
154 | alphabetic,
155 | /// The hanging baseline
156 | hanging,
157 | /// The mathematical baseline
158 | mathematical,
159 | /// The central baseline
160 | central,
161 | /// The middle baseline
162 | middle,
163 | /// The text after edge baseline
164 | text_after_edge,
165 | /// The text before edge baseline
166 | text_before_edge,
167 |
168 | pub fn format(self: DominantBaseline, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
169 | _ = fmt;
170 | _ = options;
171 | switch (self) {
172 | .auto => try writer.writeAll("auto"),
173 | .use_script => try writer.writeAll("use-script"),
174 | .no_change => try writer.writeAll("no-change"),
175 | .reset_size => try writer.writeAll("reset-size"),
176 | .ideographic => try writer.writeAll("ideographic"),
177 | .alphabetic => try writer.writeAll("alphabetic"),
178 | .hanging => try writer.writeAll("hanging"),
179 | .mathematical => try writer.writeAll("mathematical"),
180 | .central => try writer.writeAll("central"),
181 | .middle => try writer.writeAll("middle"),
182 | .text_after_edge => try writer.writeAll("text-after-edge"),
183 | .text_before_edge => try writer.writeAll("text-before-edge"),
184 | }
185 | }
186 | };
187 |
188 | /// The options of a Text.
189 | pub const Options = struct {
190 | /// The x coordinate of the text.
191 | x: LengthPercent = .{ .pixel = 0.0 },
192 | /// The y coordinate of the text.
193 | y: LengthPercent = .{ .pixel = 0.0 },
194 | /// The x displacement of the text
195 | dx: LengthPercent = .{ .pixel = 0.0 },
196 | /// The y displacement of the text
197 | dy: LengthPercent = .{ .pixel = 0.0 },
198 | /// The length of the text
199 | length: ?LengthPercent = null,
200 | /// The color of the fill of the text
201 | fill: ?RGB = null,
202 | /// The opacity of the fill of the text
203 | fill_opacity: f32 = 1.0,
204 | /// The color of the stroke of the text
205 | stroke: ?RGB = null,
206 | /// The opacity of the stroke of the text
207 | stroke_opacity: f32 = 1.0,
208 | /// The width of the stroke of the text
209 | stroke_width: LengthPercent = .{ .pixel = 1.0 },
210 | /// The opacity of the text (fill + stroke)
211 | opacity: f32 = 1.0,
212 | /// The text to display
213 | text: []const u8 = "",
214 | /// The allocator of the text (null means not allocated)
215 | allocator: ?Allocator = null,
216 | /// The font family of the text
217 | font_family: []const u8 = "sans-serif",
218 | /// The font size of the text
219 | font_size: FontSize = .medium,
220 | /// The font weight of the text,
221 | font_weight: FontWeight = .normal,
222 | /// The anchor of the text
223 | text_anchor: TextAnchor = .start,
224 | /// The dominant baseline of the text
225 | dominant_baseline: DominantBaseline = .auto,
226 | };
227 |
228 | /// The options of the Text
229 | options: Options,
230 |
231 | /// Initialize the Text with the given options
232 | pub fn init(options: Options) Text {
233 | return Text {
234 | .options = options,
235 | };
236 | }
237 |
238 | /// Deinitialize the Text
239 | pub fn deinit(self: *const Text) void {
240 | if (self.options.allocator) |allocator| {
241 | allocator.free(self.options.text);
242 | }
243 | }
244 |
245 | /// Write the text to the given writer
246 | pub fn writeTo(self: *const Text, writer: anytype) anyerror!void {
247 | try writer.writeAll("6}\" ", .{fill})
255 | else try writer.writeAll("fill=\"none\" ");
256 | try writer.print("fill-opacity=\"{d}\" ", .{self.options.fill_opacity});
257 | if (self.options.stroke) |stroke| try writer.print("stroke=\"#{X:0>6}\" ", .{stroke})
258 | else try writer.writeAll("stroke=\"none\" ");
259 | try writer.print("stroke-opacity=\"{d}\" ", .{self.options.stroke_opacity});
260 | try writer.print("stroke-width=\"{}\" ", .{self.options.stroke_width});
261 | try writer.print("opacity=\"{d}\" ", .{self.options.opacity});
262 | try writer.print("font-family=\"{s}\" ", .{self.options.font_family});
263 | try writer.print("font-size=\"{}\" ", .{self.options.font_size});
264 | try writer.print("font-weight=\"{}\" ", .{self.options.font_weight});
265 | try writer.print("text-anchor=\"{}\" ", .{self.options.text_anchor});
266 | try writer.print("dominant-baseline=\"{}\" ", .{self.options.dominant_baseline});
267 | try writer.print(">{s}", .{self.options.text});
268 | }
269 |
270 | /// Wrap the text in a kind
271 | pub fn wrap(self: *const Text) Kind {
272 | return Kind {
273 | .text = self.*
274 | };
275 | }
--------------------------------------------------------------------------------
/src/svg/kind.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const SVG = @import("SVG.zig");
4 |
5 | /// The different kind of SVG Components
6 | pub const Kind = union(enum) {
7 | /// A List of Kind
8 | pub const List = std.ArrayList(Kind);
9 |
10 | /// The Line SVG Component
11 | line: SVG.Line,
12 | /// The Rect SVG Component
13 | rect: SVG.Rect,
14 | /// The Circle SVG Component
15 | circle: SVG.Circle,
16 | /// The Polyline SVG Component
17 | polyline: SVG.Polyline,
18 | /// The Text SVG Component
19 | text: SVG.Text,
20 | /// The Path SVG Component
21 | path: SVG.Path,
22 |
23 | /// Write the Kind to the given writer
24 | pub fn writeTo(self: *const Kind, writer: anytype) anyerror!void {
25 | try switch (self.*) {
26 | .line => |line| line.writeTo(writer),
27 | .rect => |rect| rect.writeTo(writer),
28 | .circle => |circle| circle.writeTo(writer),
29 | .polyline => |polyline| polyline.writeTo(writer),
30 | .text => |text| text.writeTo(writer),
31 | .path => |path| path.writeTo(writer),
32 | };
33 | }
34 |
35 | /// Deinitialize the Kind
36 | pub fn deinit(self: *const Kind) void {
37 | switch (self.*) {
38 | .polyline => |polyline| polyline.deinit(),
39 | .text => |text| text.deinit(),
40 | .path => |path| path.deinit(),
41 | else => {},
42 | }
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/svg/util/length.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | /// The type corresponding to the SVG ||auto type.
4 | pub const LengthPercentAuto = union(enum) {
5 | /// The length in pixels
6 | pixel: f32,
7 | /// The length in percent (of the parent)
8 | percent: f32,
9 | /// Automatic length
10 | auto: void,
11 |
12 | pub fn format(self: LengthPercentAuto, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
13 | _ = fmt;
14 | _ = options;
15 | switch (self) {
16 | .pixel => |value| try writer.print("{d}", .{value}),
17 | .percent => |value| try writer.print("{d}%", .{value}),
18 | .auto => try writer.writeAll("auto"),
19 | }
20 | }
21 | };
22 |
23 | /// The type corresponding to the SVG | type.
24 | pub const LengthPercent = union(enum) {
25 | /// The length in pixels.
26 | pixel: f32,
27 | /// The length in percent (of the parent).
28 | percent: f32,
29 |
30 | pub fn format(self: LengthPercent, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
31 | _ = fmt;
32 | _ = options;
33 | switch (self) {
34 | .pixel => |value| try writer.print("{d}", .{value}),
35 | .percent => |value| try writer.print("{d}%", .{value}),
36 | }
37 | }
38 | };
--------------------------------------------------------------------------------
/src/svg/util/rgb.zig:
--------------------------------------------------------------------------------
1 | /// The type of an RGB color.
2 | pub const RGB = u48;
3 |
4 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
5 | // Predefined Colors //
6 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7 |
8 | pub const WHITE: RGB = 0xFFFFFF;
9 | pub const LIGHT_GRAY: RGB = 0xC0C0C0;
10 | pub const GRAY: RGB = 0x808080;
11 | pub const DARK_GRAY: RGB = 0x404040;
12 | pub const BLACK: RGB = 0x000000;
13 |
14 | pub const RED: RGB = 0xFF0000;
15 | pub const GREEN: RGB = 0x00FF00;
16 | pub const BLUE: RGB = 0x0000FF;
17 |
18 | pub const YELLOW: RGB = 0xFFFF00;
19 | pub const MAGENTA: RGB = 0xFF00FF;
20 | pub const CYAN: RGB = 0x00FFFF;
21 | pub const ORANGE: RGB = 0xFFC800;
22 | pub const PINK: RGB = 0xFFAFAF;
23 | pub const PURPLE: RGB = 0x800080;
24 | pub const BROWN: RGB = 0x964B00;
25 | pub const GOLD: RGB = 0xFFD700;
--------------------------------------------------------------------------------
/src/util/log.zig:
--------------------------------------------------------------------------------
1 | /// A ghost logger used for testing to ignore any outputs
2 | pub const GhostLogger = struct {
3 | pub fn err(
4 | comptime format: []const u8,
5 | args: anytype,
6 | ) void {
7 | _ = format;
8 | _ = args;
9 | }
10 |
11 | pub fn warn(
12 | comptime format: []const u8,
13 | args: anytype,
14 | ) void {
15 | _ = format;
16 | _ = args;
17 | }
18 |
19 | pub fn info(
20 | comptime format: []const u8,
21 | args: anytype,
22 | ) void {
23 | _ = format;
24 | _ = args;
25 | }
26 |
27 | pub fn debug(
28 | comptime format: []const u8,
29 | args: anytype,
30 | ) void {
31 | _ = format;
32 | _ = args;
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/util/polyshape.zig:
--------------------------------------------------------------------------------
1 | //! Utility module to generate the points of shape (to use in a polyline)
2 |
3 | const std = @import("std");
4 | const Allocator = std.mem.Allocator;
5 |
6 | /// Generate the points of a triangle (facing upwards)
7 | pub fn triangle(allocator: Allocator, center_x: f32, center_y: f32, radius: f32) ![]f32 {
8 | const points = try allocator.alloc(f32, 8);
9 | points[0] = center_x;
10 | points[1] = center_y - radius;
11 | points[2] = center_x - radius;
12 | points[3] = center_y + radius;
13 | points[4] = center_x + radius;
14 | points[5] = center_y + radius;
15 | points[6] = points[0];
16 | points[7] = points[1];
17 | return points;
18 | }
19 |
20 | // Generate the points of a rhombus (facing upwards)
21 | pub fn rhombus(allocator: Allocator, center_x: f32, center_y: f32, radius: f32) ![]f32 {
22 | const points = try allocator.alloc(f32, 10);
23 | points[0] = center_x;
24 | points[1] = center_y - radius;
25 | points[2] = center_x - radius;
26 | points[3] = center_y;
27 | points[4] = center_x;
28 | points[5] = center_y + radius;
29 | points[6] = center_x + radius;
30 | points[7] = center_y;
31 | points[8] = points[0];
32 | points[9] = points[1];
33 | return points;
34 | }
35 |
36 | /// Generate the points of a plus (+)
37 | pub fn plus(allocator: Allocator, center_x: f32, center_y: f32, radius: f32) ![]f32 {
38 | const points = try allocator.alloc(f32, 26);
39 | points[0] = center_x - radius / 4; // Top - Left
40 | points[1] = center_y - radius;
41 | points[2] = center_x + radius / 4; // Top - Right
42 | points[3] = center_y - radius;
43 | points[4] = center_x + radius / 4; // Inner - Top Right
44 | points[5] = center_y - radius / 4;
45 | points[6] = center_x + radius; // Center - Top Right
46 | points[7] = center_y - radius / 4;
47 | points[8] = center_x + radius; // Center - Bottom Right
48 | points[9] = center_y + radius / 4;
49 | points[10] = center_x + radius / 4; // Inner - Bottom Right
50 | points[11] = center_y + radius / 4;
51 | points[12] = center_x + radius / 4; // Bottom - Right
52 | points[13] = center_y + radius;
53 | points[14] = center_x - radius / 4; // Bottom - Left
54 | points[15] = center_y + radius;
55 | points[16] = center_x - radius / 4; // Inner - Bottom Left
56 | points[17] = center_y + radius / 4;
57 | points[18] = center_x - radius; // Center - Bottom Left
58 | points[19] = center_y + radius / 4;
59 | points[20] = center_x - radius; // Center - Top Left
60 | points[21] = center_y - radius / 4;
61 | points[22] = center_x - radius / 4; // Inner - Top Left
62 | points[23] = center_y - radius / 4;
63 | points[24] = points[0];
64 | points[25] = points[1];
65 | return points;
66 | }
67 |
68 | /// Generate the points of a cross (x)
69 | pub fn cross(allocator: Allocator, center_x: f32, center_y: f32, radius: f32) ![]f32 {
70 | const points = try allocator.alloc(f32, 34);
71 | points[0] = center_x - radius;
72 | points[1] = center_y - radius;
73 | points[2] = center_x - radius + radius / 4;
74 | points[3] = center_y - radius;
75 | points[4] = center_x;
76 | points[5] = center_y - radius / 4;
77 | points[6] = center_x + radius - radius / 4;
78 | points[7] = center_y - radius;
79 | points[8] = center_x + radius;
80 | points[9] = center_y - radius;
81 | points[10] = center_x + radius;
82 | points[11] = center_y - radius + radius / 4;
83 | points[12] = center_x + radius / 4;
84 | points[13] = center_y;
85 | points[14] = center_x + radius;
86 | points[15] = center_y + radius - radius / 4;
87 | points[16] = center_x + radius;
88 | points[17] = center_y + radius;
89 | points[18] = center_x + radius - radius / 4;
90 | points[19] = center_y + radius;
91 | points[20] = center_x;
92 | points[21] = center_y + radius / 4;
93 | points[22] = center_x - radius + radius / 4;
94 | points[23] = center_y + radius;
95 | points[24] = center_x - radius;
96 | points[25] = center_y + radius;
97 | points[26] = center_x - radius;
98 | points[27] = center_y + radius - radius / 4;
99 | points[28] = center_x - radius / 4;
100 | points[29] = center_y;
101 | points[30] = center_x - radius;
102 | points[31] = center_y - radius + radius / 4;
103 | points[32] = points[0];
104 | points[33] = points[1];
105 | return points;
106 | }
--------------------------------------------------------------------------------
/src/util/range.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | /// A range of values.
4 | pub fn Range(comptime T: type) type {
5 | return struct {
6 | const Self = @This();
7 |
8 | min: T,
9 | max: T,
10 |
11 | /// Initialize a range with the given [min] and [max] values. [min; max]
12 | pub fn init(min: T, max: T) Self {
13 | return Self{
14 | .min = min,
15 | .max = max,
16 | };
17 | }
18 |
19 | /// Initialize a range with the minimum and maximum values set to the same value. [-∞; ∞]
20 | pub fn inf() Self {
21 | if (@typeInfo(T) != .Float) @compileError("Only floating point types can have infinite ranges");
22 |
23 | return Self{
24 | .min = -std.math.inf(T),
25 | .max = std.math.inf(T),
26 | };
27 | }
28 |
29 | /// Initialize a range with the minimum and maximum values set to the same value. [∞; -∞]
30 | pub fn invInf() Self {
31 | if (@typeInfo(T) != .Float) @compileError("Only floating point types can have infinite ranges");
32 |
33 | return Self{
34 | .min = std.math.inf(T),
35 | .max = -std.math.inf(T),
36 | };
37 | }
38 |
39 | /// Check if the range contains the given [value].
40 | pub fn contains(self: *const Self, value: T) bool {
41 | return value >= self.min and value <= self.max;
42 | }
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/util/scale.zig:
--------------------------------------------------------------------------------
1 | pub const Scale = enum {
2 | linear,
3 | log,
4 | };
5 |
--------------------------------------------------------------------------------
/src/util/shape.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 |
4 | const SVG = @import("../svg/SVG.zig");
5 | const RGB = @import("../svg/util/rgb.zig").RGB;
6 |
7 | const polyshape = @import("polyshape.zig");
8 |
9 | /// The enumeration of shape
10 | pub const Shape = enum {
11 | circle,
12 | circle_outline,
13 | square,
14 | square_outline,
15 | triangle,
16 | triangle_outline,
17 | rhombus,
18 | rhombus_outline,
19 | plus,
20 | plus_outline,
21 | cross,
22 | cross_outline,
23 |
24 | /// Write the shape to the given SVG
25 | pub fn writeTo(self: Shape, allocator: Allocator, svg: *SVG, x: f32, y: f32, radius: f32, color: RGB) !void {
26 | switch (self) {
27 | .circle => try svg.addCircle(.{
28 | .center_x = .{ .pixel = x },
29 | .center_y = .{ .pixel = y },
30 | .radius = .{ .pixel = radius },
31 | .fill = color,
32 | }),
33 | .circle_outline => try svg.addCircle(.{
34 | .center_x = .{ .pixel = x },
35 | .center_y = .{ .pixel = y },
36 | .radius = .{ .pixel = radius },
37 | .fill = null,
38 | .stroke = color,
39 | .stroke_width = .{ .pixel = radius / 4 }
40 | }),
41 | .square => try svg.addRect(.{
42 | .x = .{ .pixel = x - radius },
43 | .y = .{ .pixel = y - radius },
44 | .width = .{ .pixel = radius * 2 },
45 | .height = .{ .pixel = radius * 2 },
46 | .fill = color
47 | }),
48 | .square_outline => try svg.addRect(.{
49 | .x = .{ .pixel = x - radius },
50 | .y = .{ .pixel = y - radius },
51 | .width = .{ .pixel = radius * 2 },
52 | .height = .{ .pixel = radius * 2 },
53 | .fill = null,
54 | .stroke = color,
55 | .stroke_width = .{ .pixel = radius / 4 }
56 | }),
57 | .triangle => {
58 | const points = try polyshape.triangle(allocator, x, y, radius);
59 | try svg.addPolyline(.{
60 | .points = points,
61 | .fill = color,
62 | });
63 | },
64 | .triangle_outline => {
65 | const points = try polyshape.triangle(allocator, x, y, radius);
66 | try svg.addPolyline(.{
67 | .points = points,
68 | .stroke = color,
69 | .stroke_width = .{ .pixel = radius / 4 }
70 | });
71 | },
72 | .rhombus => {
73 | const points = try polyshape.rhombus(allocator, x, y, radius);
74 | try svg.addPolyline(.{
75 | .points = points,
76 | .fill = color,
77 | });
78 | },
79 | .rhombus_outline => {
80 | const points = try polyshape.rhombus(allocator, x, y, radius);
81 | try svg.addPolyline(.{
82 | .points = points,
83 | .stroke = color,
84 | .stroke_width = .{ .pixel = radius / 4 }
85 | });
86 | },
87 | .plus => {
88 | const points = try polyshape.plus(allocator, x, y, radius);
89 |
90 | try svg.addPolyline(.{
91 | .points = points,
92 | .fill = color,
93 | });
94 | },
95 | .plus_outline => {
96 | const points = try polyshape.plus(allocator, x, y, radius);
97 |
98 | try svg.addPolyline(.{
99 | .points = points,
100 | .stroke = color,
101 | .stroke_width = .{ .pixel = radius / 4 }
102 | });
103 | },
104 | .cross => {
105 | const points = try polyshape.cross(allocator, x, y, radius);
106 |
107 | try svg.addPolyline(.{
108 | .points = points,
109 | .fill = color,
110 | });
111 | },
112 | .cross_outline => {
113 | const points = try polyshape.cross(allocator, x, y, radius);
114 |
115 | try svg.addPolyline(.{
116 | .points = points,
117 | .stroke = color,
118 | .stroke_width = .{ .pixel = radius / 4 }
119 | });
120 | },
121 | }
122 | }
123 | };
--------------------------------------------------------------------------------
/src/util/units.zig:
--------------------------------------------------------------------------------
1 | /// A value or a percentage.
2 | pub const ValuePercent = union(enum) {
3 | value: f32,
4 | percent: f32,
5 | };
6 |
7 | /// A padding for the values.
8 | pub const ValuePadding = struct {
9 | x_max: ValuePercent = .{ .percent = 0.0 },
10 | y_max: ValuePercent = .{ .percent = 0.1 },
11 | x_min: ValuePercent = .{ .percent = 0.0 },
12 | y_min: ValuePercent = .{ .percent = 0.1 },
13 | };
14 |
15 | /// A value in pixels or an auto gap.
16 | pub const PixelAutoGap = union(enum) {
17 | pixel: f32,
18 | auto_gap: f32,
19 | };
20 |
21 | /// A count of values or the gap between them.
22 | pub const CountGap = union(enum) {
23 | count: usize,
24 | gap: f32,
25 | };
26 |
27 | /// A position in the corner.
28 | pub const CornerPosition = enum {
29 | top_left,
30 | top_right,
31 | bottom_left,
32 | bottom_right,
33 | };
34 |
--------------------------------------------------------------------------------