charton 0.5.9

A high-performance, layered charting system for Rust, featuring a flexible data core and multi-backend rendering.
Documentation
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# Decoupled Rendering (winit & wgpu)


If you are building an application where chart updates are infrequent, or if you are struggling with `wgpu` version conflicts between your UI framework and Charton, Decoupled Rendering is the solution.

Using `WgpuRenderer`, Charton acts as a completely isolated black box. It initializes its own invisible WGPU instance, renders the chart offscreen, reads the pixels back to CPU memory, composites the text, and hands you a raw `Vec<u8>` RGBA buffer.

## The GPU → CPU → GPU Roundtrip


This architecture trades raw performance for ultimate architectural safety. Your host application simply takes the CPU bytes and uploads them to its own GPU texture for display.

Here is a step-by-step, hands-on tutorial to help you build a complete project using Charton's decoupled rendering architecture.This architecture trades raw performance for ultimate architectural safety. Your host application simply takes the CPU bytes generated by Charton and uploads them to its own GPU texture for display.  Let's build it from scratch!

### Step 1: Create the Project


Open your terminal and create a new Rust binary project:

```bash
cargo new winit_example
cd winit_example
```

### Step 2: Configure `Cargo.toml`


Next, we need to declare our dependencies. We will use `winit` to create the application window, `wgpu` to manage the host graphics context, and `pollster` to run our asynchronous setup code.

Open your `Cargo.toml` file and replace its contents with the following:

```toml
[package]
name = "winit_example"
version = "0.1.0"
edition = "2024"

[dependencies]
charton = { version = "0.5", features = ["wgpu", "png"] }
winit = "0.29"
wgpu = "29"
pollster = "0.4"
```

### Step 3: Write the Application Code


Now, open `src/main.rs`. We are going to build the application in three logical phases:

* The Shader Pipeline: Creating a "Full-Screen Triangle" shader to paint our texture.
* Phase 1 (Charton's GPU): Setting up Charton to render offscreen.
* Phase 2 (Host App GPU): Setting up the window and the event loop to display the pixels.

```rust
use std::sync::Arc;
use charton::prelude::*;
use charton::render::WgpuRenderer;
use winit::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};

/// Creates a minimal render pipeline to display a 2D texture on the screen.
/// 
/// This uses a common graphics trick: a "Full-Screen Triangle". 
/// Instead of creating a vertex buffer with a quad (4 vertices), we hardcode 
/// 3 oversized vertices in the vertex shader (`vs_main`) that cover the entire viewport.
/// This avoids the overhead of managing vertex/index buffers.
fn create_present_pipeline(
    device: &wgpu::Device,
    surface_format: wgpu::TextureFormat,
) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("charton present shader"),
        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
            struct VertexOutput {
                @builtin(position) position: vec4<f32>,
                @location(0) uv: vec2<f32>,
            }

            @vertex
            fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
                // Hardcoded oversized triangle that covers the entire NDC (Normalized Device Coordinates) space
                var positions = array<vec2<f32>, 3>(
                    vec2<f32>(-1.0, -1.0),
                    vec2<f32>(3.0, -1.0),
                    vec2<f32>(-1.0, 3.0),
                );
                // Corresponding UV coordinates for mapping the texture
                var uvs = array<vec2<f32>, 3>(
                    vec2<f32>(0.0, 1.0),
                    vec2<f32>(2.0, 1.0),
                    vec2<f32>(0.0, -1.0),
                );

                var out: VertexOutput;
                out.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
                out.uv = uvs[vertex_index];
                return out;
            }

            @group(0) @binding(0) var source_texture: texture_2d<f32>;
            @group(0) @binding(1) var source_sampler: sampler;

            @fragment
            fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
                // Sample the charton-generated texture and output to screen
                return textureSampleLevel(source_texture, source_sampler, in.uv, 0.0);
            }
        "#)),
    });

    let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
        label: Some("charton present bind group layout"),
        entries: &[
            // Texture binding
            wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::FRAGMENT,
                ty: wgpu::BindingType::Texture {
                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
                    view_dimension: wgpu::TextureViewDimension::D2,
                    multisampled: false,
                },
                count: None,
            },
            // Sampler binding
            wgpu::BindGroupLayoutEntry {
                binding: 1,
                visibility: wgpu::ShaderStages::FRAGMENT,
                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                count: None,
            },
        ],
    });

    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: Some("charton present pipeline layout"),
        bind_group_layouts: &[Some(&bind_group_layout)],
        ..Default::default()
    });

    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some("charton present pipeline"),
        layout: Some(&pipeline_layout),
        vertex: wgpu::VertexState {
            module: &shader,
            entry_point: Some("vs_main"),
            compilation_options: Default::default(),
            buffers: &[], // Empty! Using the hardcoded vertices in the shader.
        },
        fragment: Some(wgpu::FragmentState {
            module: &shader,
            entry_point: Some("fs_main"),
            compilation_options: Default::default(),
            targets: &[Some(wgpu::ColorTargetState {
                format: surface_format,
                blend: None,
                write_mask: wgpu::ColorWrites::ALL,
            })],
        }),
        primitive: Default::default(),
        depth_stencil: None,
        multisample: wgpu::MultisampleState {
            count: 1,
            mask: !0,
            alpha_to_coverage_enabled: false,
        },
        multiview_mask: std::num::NonZeroU32::new(0),
        cache: None,
    });

    (pipeline, bind_group_layout)
}

async fn run() {
    let event_loop = EventLoop::new().unwrap();

    let window = Arc::new(
        WindowBuilder::new()
            .with_title("Charton WgpuRenderer Example")
            .with_inner_size(winit::dpi::LogicalSize::new(800.0, 600.0))
            .build(&event_loop)
            .unwrap(),
    );

    // ==========================================
    // Phase 1: Chart Rendering (Headless & Decoupled)
    // ==========================================
    // WgpuRenderer creates its OWN wgpu device/queue internally.
    let mut charton_renderer = WgpuRenderer::new();

    // Build the declarative chart
    let ds = load_dataset("mtcars").unwrap();
    let chart = Chart::build(&ds)
        .unwrap()
        .mark_point()
        .unwrap()
        .configure_point(|p| p.with_size(10.0))
        .encode((
            alt::x("wt"),
            alt::y("mpg"),
            alt::color("gear").with_scale(Scale::Discrete),
        ))
        .unwrap()
        .configure_theme(|t| t.with_label_size(20.0).with_tick_label_size(18.0))
        .with_size(800, 600);

    // ==========================================
    // Phase 2: Window Presentation (App-owned WGPU Context)
    // ==========================================
    // We create a SECOND wgpu instance specifically for the host window.
    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
        backends: wgpu::Backends::all(),
        flags: wgpu::InstanceFlags::default(),
        backend_options: wgpu::BackendOptions::default(),
        memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
        display: None,
    });

    let surface = instance.create_surface(window.clone()).unwrap();

    let adapter = instance
        .request_adapter(&wgpu::RequestAdapterOptions {
            compatible_surface: Some(&surface),
            ..Default::default()
        })
        .await
        .unwrap();

    let (device, queue) = adapter
        .request_device(&wgpu::DeviceDescriptor::default())
        .await
        .unwrap();

    let size = window.inner_size();
    let capabilities = surface.get_capabilities(&adapter);
    let surface_format = capabilities.formats[0].remove_srgb_suffix();
    let mut config = surface
        .get_default_config(&adapter, size.width, size.height)
        .unwrap();
    config.format = surface_format;
    surface.configure(&device, &config);

    let (present_pipeline, bind_group_layout) =
        create_present_pipeline(&device, surface_format);

    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
        mag_filter: wgpu::FilterMode::Linear,
        min_filter: wgpu::FilterMode::Linear,
        mipmap_filter: wgpu::MipmapFilterMode::Linear,
        lod_min_clamp: 0.0,
        lod_max_clamp: 1.0,
        compare: None,
        anisotropy_clamp: 16,
        ..Default::default()
    });

    let _ = event_loop.run(move |event, target| {
        target.set_control_flow(ControlFlow::Poll);

        match event {
            Event::WindowEvent {
                event: WindowEvent::CloseRequested,
                ..
            } => target.exit(),
            Event::WindowEvent {
                event: WindowEvent::RedrawRequested,
                ..
            } => {
                // 1. Render Chart -> CPU Pixels
                // Charton's internal GPU renders the chart, reads it back, composites text, 
                // and outputs raw RGBA bytes. 
                let pixels = charton_renderer.render(&chart, 800, 600, 1.0).unwrap();

                // 2. Upload CPU Pixels -> App GPU Texture
                // NOTE: This GPU -> CPU -> GPU roundtrip is perfect for simple tools or low-FPS apps.
                // For high-performance interactive apps (like Bevy/egui), users should use 
                // `chart.render_to_surface()` to avoid this memory readback entirely.
                let tex = device.create_texture(&wgpu::TextureDescriptor {
                    label: Some("charton output texture"),
                    size: wgpu::Extent3d {
                        width: 800,
                        height: 600,
                        depth_or_array_layers: 1,
                    },
                    mip_level_count: 1,
                    sample_count: 1,
                    dimension: wgpu::TextureDimension::D2,
                    format: wgpu::TextureFormat::Rgba8Unorm,
                    usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
                    view_formats: &[],
                });
                
                queue.write_texture(
                    wgpu::TexelCopyTextureInfo {
                        texture: &tex,
                        mip_level: 0,
                        origin: wgpu::Origin3d::ZERO,
                        aspect: wgpu::TextureAspect::All,
                    },
                    &pixels,
                    wgpu::TexelCopyBufferLayout {
                        offset: 0,
                        bytes_per_row: Some(800 * 4),
                        rows_per_image: Some(600),
                    },
                    wgpu::Extent3d {
                        width: 800,
                        height: 600,
                        depth_or_array_layers: 1,
                    },
                );

                let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
                let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
                    label: Some("charton present bind group"),
                    layout: &bind_group_layout,
                    entries: &[
                        wgpu::BindGroupEntry {
                            binding: 0,
                            resource: wgpu::BindingResource::TextureView(&view),
                        },
                        wgpu::BindGroupEntry {
                            binding: 1,
                            resource: wgpu::BindingResource::Sampler(&sampler),
                        },
                    ],
                });

                // 3. Draw Texture to Screen
                let frame = match surface.get_current_texture() {
                    wgpu::CurrentSurfaceTexture::Success(texture) => texture,
                    wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
                    wgpu::CurrentSurfaceTexture::Timeout
                    | wgpu::CurrentSurfaceTexture::Occluded => {
                        return;
                    }
                    wgpu::CurrentSurfaceTexture::Outdated
                    | wgpu::CurrentSurfaceTexture::Lost => {
                        return;
                    }
                    _ => return,
                };

                let frame_view =
                    frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
                let mut encoder = device.create_command_encoder(
                    &wgpu::CommandEncoderDescriptor::default(),
                );
                {
                    let mut rpass =
                        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                            label: Some("charton present render pass"),
                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                                view: &frame_view,
                                resolve_target: None,
                                ops: wgpu::Operations {
                                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                                    store: wgpu::StoreOp::Store,
                                },
                                depth_slice: None,
                            })],
                            depth_stencil_attachment: None,
                            timestamp_writes: None,
                            occlusion_query_set: None,
                            multiview_mask: std::num::NonZeroU32::new(0),
                        });
                    rpass.set_pipeline(&present_pipeline);
                    rpass.set_bind_group(0, &bind_group, &[]);
                    // Draw 3 vertices for our hardcoded full-screen triangle
                    rpass.draw(0..3, 0..1); 
                }
                queue.submit(Some(encoder.finish()));
                frame.present();
            }
            Event::AboutToWait => {
                window.request_redraw();
            }
            _ => {}
        }
    });
}

fn main() {
    pollster::block_on(run());
}
```

### Step 4: Run the Application


Now that your files are set up, open your terminal and start the application:

```bash
cargo run
```

You should see a new window pop up titled "Charton WgpuRenderer Example" displaying the scattered points from the mtcars dataset!

## Important Performance Considerations & Architectural Trade-offs


Notice how `charton_renderer.render(&chart, 800, 600, 1.0).unwrap()` is called inside the `RedrawRequested` event block. This means the chart is being completely re-rendered, read from the GPU to the CPU, and sent back to the host App's GPU on every single frame.

### Why this compromise? (The WGPU Dependency Hell)


This GPU → CPU → GPU roundtrip is an intentional architectural trade-off. In the Rust ecosystem, sharing a GPU context requires both the host application and the rendering library to depend on the exact same version of `wgpu`. By returning raw CPU bytes (`Vec<u8>`) instead of a GPU texture, Charton acts as a completely isolated black box. You can use `wgpu v0.29` in your app while Charton internally uses `wgpu v0.28`, and they will never conflict.

### The Performance Tip


Because of the heavy memory readback overhead, this decoupled approach is designed for tools, static dashboards, or low-FPS applications.

* In production: You should cache the `Vec<u8>` output. Only call `charton_renderer.render()` when your dataset or chart configuration actually changes. Do not run it blindly on every `RedrawRequested` event.
* Need 60 FPS? If you are building a high-performance interactive application (like a game in Bevy or a fluid egui dashboard) and can guarantee `wgpu` version alignment, you should abandon this decoupled method and use `chart.render_to_surface()` for true Zero-Copy GPU rendering.