├── .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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 0.00 25 | 4.50 26 | 9.00 27 | 13.50 28 | 18.00 29 | 22.50 30 | -0.00 31 | 0.37 32 | 0.73 33 | 1.10 34 | 1.46 35 | 1.83 36 | -0.37 37 | -0.73 38 | -1.10 39 | -------------------------------------------------------------------------------- /example/out/candlestick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CandleStick example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 0.81 29 | 1.24 30 | 1.68 31 | 2.11 32 | 2.54 33 | 2.98 34 | -------------------------------------------------------------------------------- /example/out/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 0.00 25 | 4.67 26 | 9.33 27 | 14.00 28 | 18.67 29 | 23.33 30 | -0.00 31 | 0.37 32 | 0.73 33 | 1.10 34 | 1.46 35 | 1.83 36 | -0.37 37 | -0.73 38 | -1.10 39 | 40 | 41 | Bottom 42 | -------------------------------------------------------------------------------- /example/out/scatter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0.00 75 | 4.67 76 | 9.33 77 | 14.00 78 | 18.67 79 | 23.33 80 | 0.78 81 | 6.05 82 | 11.32 83 | 16.59 84 | 21.86 85 | 27.13 86 | -------------------------------------------------------------------------------- /example/out/stem.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 0.00 53 | 2.33 54 | 4.67 55 | 7.00 56 | 9.33 57 | 11.67 58 | 0.00 59 | 0.18 60 | 0.37 61 | 0.55 62 | 0.73 63 | 0.91 64 | -0.18 65 | -0.37 66 | -0.55 67 | -0.73 68 | -0.91 69 | -------------------------------------------------------------------------------- /example/out/step.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0.00 66 | 2.33 67 | 4.67 68 | 7.00 69 | 9.33 70 | 11.67 71 | 0.00 72 | 0.18 73 | 0.37 74 | 0.55 75 | 0.73 76 | 0.91 77 | -0.18 78 | -0.37 79 | -0.55 80 | -0.73 81 | -0.91 82 | -------------------------------------------------------------------------------- /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 | ![Example Plot](out.svg) 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 | ![Line Plot](example/out/line.svg) 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 | ![Area Plot](example/out/area.svg) 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 | ![Scatter Plot](example/out/scatter.svg) 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 | ![Step Plot](example/out/step.svg) 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 | ![Stem Plot](example/out/stem.svg) 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 | ![Candlestick](example/out/candlestick.svg) 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 | \\ 88 | \\ 89 | ; 90 | 91 | /// Write the SVG to the given writer 92 | pub fn writeTo(self: *const SVG, writer: anytype) anyerror!void { 93 | // Write the header 94 | try writer.print(SVG_HEADER, .{ 95 | self.width, 96 | self.height, 97 | self.viewbox.x, 98 | self.viewbox.y, 99 | self.viewbox.width, 100 | self.viewbox.height, 101 | }); 102 | // Write the data 103 | for (self.data.items) |kind| { 104 | try kind.writeTo(writer); 105 | try writer.writeByte('\n'); 106 | } 107 | // End of the SVG 108 | try writer.writeAll(""); 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 | --------------------------------------------------------------------------------