Metadata-Version: 2.4
Name: rvgsrust-xlsxwriter
Version: 0.2.3
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Dist: pandas>=2.0 ; extra == 'all'
Requires-Dist: polars>=1.0 ; extra == 'all'
Requires-Dist: pyarrow>=14.0 ; extra == 'all'
Requires-Dist: pytest>=8.0 ; extra == 'dev'
Requires-Dist: pytest-benchmark>=4.0 ; extra == 'dev'
Requires-Dist: black>=24.0 ; extra == 'dev'
Requires-Dist: ruff>=0.4 ; extra == 'dev'
Requires-Dist: openpyxl>=3.1 ; extra == 'dev'
Requires-Dist: pyarrow>=14.0 ; extra == 'dev'
Requires-Dist: pandas>=2.0 ; extra == 'pandas'
Requires-Dist: pyarrow>=14.0 ; extra == 'pandas'
Requires-Dist: polars>=1.0 ; extra == 'polars'
Provides-Extra: all
Provides-Extra: dev
Provides-Extra: pandas
Provides-Extra: polars
License-File: LICENSE
Summary: A Rust-powered XLSX library for Python, with a Pythonic API and Polars/Pandas/PyArrow support. Built on rust_xlsxwriter.
Keywords: excel,xlsx,spreadsheet,rust,polars,pandas,fast
Author-email: RVGS Team <rvgs@borderlessaccess.com>
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/Godse4ever/rvgsrust-xlsxwriter#readme
Project-URL: Homepage, https://github.com/Godse4ever/rvgsrust-xlsxwriter
Project-URL: Issues, https://github.com/Godse4ever/rvgsrust-xlsxwriter/issues
Project-URL: Repository, https://github.com/Godse4ever/rvgsrust-xlsxwriter

# RVGSRust-XLSXWriter

> **A Rust-powered XLSX library for Python, with a Pythonic API and Polars/Pandas/PyArrow support.**
>
> Built on the official [`rust_xlsxwriter`](https://github.com/jmcnamara/rust_xlsxwriter) crate. Not affiliated with the Python [`XlsxWriter`](https://xlsxwriter.readthedocs.io/) package (a separate, pure-Python project it shares a similar name and purpose with) -- this library's Python-facing API is inspired by it but is not a drop-in replacement; see [Quick Start](#quick-start) below for the real differences.

[![PyPI version](https://badge.fury.io/py/rvgsrust-xlsxwriter.svg)](https://pypi.org/project/rvgsrust-xlsxwriter/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust](https://img.shields.io/badge/Rust-1.85%2B-orange.svg)](https://www.rust-lang.org)
[![Python](https://img.shields.io/badge/Python-3.8%2B-blue.svg)](https://www.python.org)

---

## Why RVGSRust-XLSXWriter?

| Feature | `xlsxwriter` (Python) | `rustpy-xlsxwriter` | `rvgsrust-xlsxwriter` |
|---------|----------------------|---------------------|----------------------|
| Speed (100k rows) | 1× (baseline) | — | **6–8× faster** ⚡ |
| **GIL released during `save()`** | ❌ blocks | ❌ blocks | ✅ **Yes** (~20ms stall vs ~1,300ms on a wide workload) |
| **Cell Merging** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Full Format API** | ✅ Yes | ⚠️ Limited | ✅ **Complete** |
| **Borders (all sides)** | ✅ Yes | ⚠️ Basic | ✅ **All sides + colors** |
| **Cell/Font Colors** | ✅ Yes | ✅ Yes | ✅ **Yes** |
| **Formulas** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Hyperlinks (text/tip)** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Date/Time** | ✅ Yes | ✅ Yes | ✅ **Yes** |
| **Images** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Freeze Panes** | ✅ Yes | ✅ Yes | ✅ **Yes** |
| **Sheet Protection** | ✅ Yes | ✅ Yes | ✅ **Yes** |
| **Worksheet Tables** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Rich Strings** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Polars/Pandas/PyArrow** | Manual | ✅ Zero-copy | ✅ **Zero-copy** |
| **Charts** | ✅ Yes | ❌ No | ✅ **Yes** |
| **Conditional Format** | ✅ Yes | ❌ No | ✅ **Yes** |

**We win on completeness.** Both `rustpy-xlsxwriter` and `rvgsrust-xlsxwriter` use the same Rust core, but we expose *every* feature so you never have to fall back to Python.

The **6–8×** figure is measured against pure-Python `xlsxwriter` at 100,000 rows × 8 columns (tall, narrow shape) — see [Performance & Benchmarks](#performance--benchmarks) for full numbers and methodology. Against another Rust-backed writer with the same `rust_xlsxwriter` core (`rustpy-xlsxwriter`), independent testing on a wide workload (76,480 columns × 1,261 rows) measured a more modest **~12% faster** overall save time. Where this library won decisively on that same wide workload was responsiveness during `save()`: a background thread polling every 5ms saw a worst-case stall of **~20ms**, versus **~1,300ms** for the comparison library — the GIL is released during save, so a GUI or async app stays responsive instead of freezing. That's arguably a stronger selling point than raw throughput for desktop-app use cases, even though it's less prominent in the numbers above.

---

## Installation

```bash
pip install rvgsrust-xlsxwriter
```

### Optional Dependencies

```bash
# For Polars DataFrame support (recommended)
pip install rvgsrust-xlsxwriter[polars]

# For Pandas DataFrame support
pip install rvgsrust-xlsxwriter[pandas]

# For everything
pip install rvgsrust-xlsxwriter[all]
```

---

## Quick Start

```python
from rvgsrust_xlsxwriter import Workbook

# Create workbook -- pass the path up front so close() needs no argument later
wb = Workbook("report.xlsx")
ws = wb.add_worksheet("Sales")

# Create a rich format
header = wb.add_format()
header.set_bold()
header.set_font_size(14)
header.set_background_color("#4472C4")
header.set_font_color("white")
header.set_align("center")
header.set_border("thin")

# Write with format
ws.write(0, 0, "Product", header)
ws.write(0, 1, "Q1 Sales", header)
ws.write(0, 2, "Q2 Sales", header)

# Merge cells
ws.merge_range(0, 3, 0, 4, "Total", header)

# Write data
data_format = wb.add_format()
data_format.set_border("thin")
data_format.set_border_color("#D9D9D9")

ws.write(1, 0, "Widgets", data_format)
ws.write(1, 1, 12000, data_format)
ws.write(1, 2, 15000, data_format)
ws.write_formula(1, 3, "=B2+C2", data_format)

# Auto-fit columns
ws.autofit()

# Save -- close() with no argument uses the path given to the constructor.
# Passing a path here still works too, and overrides the constructor path.
wb.close()
```

The context-manager form does the same thing automatically on `__exit__`:

```python
with Workbook("report.xlsx") as wb:
    ws = wb.add_worksheet("Sales")
    ws.write(0, 0, "Product")
# saved to report.xlsx here, close() called for you
```

`save_to_buffer()` skips the file entirely and returns the xlsx as
`bytes` -- useful for a web response or an in-memory pipeline, with no
temp-file round trip:

```python
wb = Workbook()
ws = wb.add_worksheet()
ws.write(0, 0, "In memory")
xlsx_bytes = wb.save_to_buffer()
# e.g. return Response(xlsx_bytes, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
```

---

## Features

### Bulk Writes (recommended for large datasets)

```python
data = [
    {"Name": "Alice", "Department": "Engineering", "Salary": 95000},
    {"Name": "Bob", "Department": "Sales", "Salary": 72000},
    # ... thousands more rows
]

header_fmt = wb.add_format()
header_fmt.set_bold()

# One call for the entire dataset, instead of one write() call per cell
ws.write_records(0, 0, data, header_format=header_fmt)

# Column order/subset and header row are both optional:
ws.write_records(0, 0, data, headers=["Name", "Salary"], write_header=False)
```

### Complete Formatting

```python
fmt = wb.add_format()

# Font
fmt.set_bold()
fmt.set_italic()
fmt.set_underline()
fmt.set_font_name("Calibri")
fmt.set_font_size(12)
fmt.set_font_color("#FF0000")      # Hex or named colors

# Background
fmt.set_background_color("#FFFF00")
fmt.set_pattern("solid")           # Pattern fills

# Borders (all sides or individual)
fmt.set_border("thin")
fmt.set_border_color("#000000")
fmt.set_top_border("thick")
fmt.set_bottom_border("double")
fmt.set_left_border("dashed")
fmt.set_right_border("dotted")

# Alignment
fmt.set_align("center")            # left, center, right, fill, justify
fmt.set_vertical_align("vcenter")  # top, vcenter, bottom
fmt.set_text_wrap()
fmt.set_rotation(45)

# Numbers
fmt.set_num_format("$#,##0.00")
fmt.set_num_format("0.00%")
fmt.set_num_format("yyyy-mm-dd")

# All setters return self, so they chain:
wb.add_format().set_bold().set_font_size(12).set_background_color("#4472C4")
```

### Cell Merging

```python
# Merge range: first_row, first_col, last_row, last_col
merge_fmt = wb.add_format()
merge_fmt.set_bold()
merge_fmt.set_background_color("#7030A0")
merge_fmt.set_font_color("white")
merge_fmt.set_align("center")

ws.merge_range(0, 0, 0, 4, "Quarterly Report", merge_fmt)
ws.merge_range(2, 0, 4, 0, "Vertical Label", merge_fmt)
```

### Formulas & Hyperlinks

```python
# Formula
ws.write_formula(5, 0, "=SUM(A1:A4)")
ws.write_formula(5, 1, "=AVERAGE(B1:B4)", money_format)

# Hyperlink
ws.write_url(6, 0, "https://github.com/Godse4ever/rvgsrust-xlsxwriter")
```

### Dates & Times

```python
# Write date
ws.write_date(0, 0, 2024, 1, 15)  # year, month, day

# Write datetime
ws.write_datetime(0, 1, 2024, 1, 15, 14, 30, 0)  # + hour, min, sec

# Use date format
date_fmt = wb.add_format()
date_fmt.set_num_format("yyyy-mm-dd")
ws.write_date(0, 0, 2024, 1, 15, date_fmt)
```

### Images

```python
ws.insert_image(0, 0, "logo.png")
```

### Sheet Operations

```python
ws.set_column_width(0, 20.0)       # Set column width
ws.set_row_height(0, 30.0)         # Set row height
ws.freeze_panes(1, 0)              # Freeze header row
ws.hide()                          # Hide sheet
ws.set_tab_color("red")            # Color the sheet tab
ws.protect("password")             # Password protect
ws.autofit()                       # Auto-fit columns
ws.autofilter(0, 0, 100, 4)        # Add filter dropdowns over A1:E101
```

### Defined Names

```python
# Workbook-global name
wb.define_name("SalesTotal", "Sheet1!$B$2:$B$100")

# Sheet-scoped name (only visible/usable within that sheet)
wb.define_name("Sheet1!LocalTotal", "Sheet1!$B$2:$B$100")
```

### Tables

Write the data first with the normal `write_*()` methods, then call
`add_table()` over the range it occupies -- matches `rust_xlsxwriter`'s
own usage pattern:

```python
from rvgsrust_xlsxwriter import Table, TableColumn

ws.write_row(0, 0, ["Product", "Q1", "Q2"])
ws.write_column(1, 0, ["Apples", "Pears"])
ws.write_column(1, 1, [10, 20])
ws.write_column(1, 2, [15, 25])

columns = [
    TableColumn().set_header("Product").set_total_label("Total"),
    TableColumn().set_header("Q1").set_total_function("sum"),
    TableColumn().set_header("Q2").set_total_function("sum"),
]

table = (
    Table()
    .set_columns(columns)
    .set_name("SalesTable")
    .set_style("medium9")
    .set_total_row(True)
    .set_banded_rows(True)
)

ws.add_table(0, 0, 3, 2, table)
```

`TableColumn` also supports `set_formula()` (per-row calculated
column, using Excel's structured-reference syntax like
`"SUM(SalesTable[@[Q1]:[Q2]])"`), `set_format()`/`set_header_format()`
(per-column cell/header formatting), and `set_total_function()` accepts
either a built-in keyword (`"sum"`, `"average"`, `"count"`,
`"count_numbers"`, `"max"`, `"min"`, `"stddev"`, `"var"`, `"none"`) or,
for anything else, treats the string as a custom formula.
`Table.set_style()` accepts `"none"`, `"light1"`-`"light21"`,
`"medium1"`-`"medium28"`, or `"dark1"`-`"dark11"`.

---

## Multithreading

**Cross-sheet parallelism is already active, automatically, for every multi-sheet workbook.** The underlying `rust_xlsxwriter` crate spawns one real OS thread per worksheet to assemble its XML when you call `wb.close()`:

```rust
// From rust_xlsxwriter's own source (src/packager.rs) -- unconditional,
// no feature flag, runs on every save() with 2+ worksheets:
thread::scope(|scope| {
    for worksheet in &mut workbook.worksheets {
        scope.spawn(|| {
            worksheet.assemble_xml_file();
        });
    }
});
```

| Worksheets | Threads spawned |
|---|---|
| 1 | 1 |
| 5 | 5 |

This is automatic and scales to one thread per worksheet.

---

## Polars / Pandas Integration

`write_polars_dataframe()` / `write_pandas_dataframe()` automatically use the zero-copy Arrow path under the hood whenever possible.

### Polars (Zero-Copy)

```python
import polars as pl
from rvgsrust_xlsxwriter import Workbook
from rvgsrust_xlsxwriter.dataframe import write_polars_dataframe

df = pl.DataFrame({
    "Name": ["Alice", "Bob", "Charlie"],
    "Sales": [100.5, 200.75, 150.0],
})

wb = Workbook()
ws = wb.add_worksheet()

header_fmt = wb.add_format()
header_fmt.set_bold()
header_fmt.set_background_color("#4472C4")
header_fmt.set_font_color("white")

write_polars_dataframe(ws, df, header_format=header_fmt)
ws.autofit()
wb.close("output.xlsx")
```

### Pandas

```python
import pandas as pd
from rvgsrust_xlsxwriter import Workbook
from rvgsrust_xlsxwriter.dataframe import write_pandas_dataframe

df = pd.DataFrame({
    "Product": ["Widget", "Gadget"],
    "Price": [9.99, 19.99],
})

wb = Workbook()
ws = wb.add_worksheet()

write_pandas_dataframe(ws, df)
wb.close("output.xlsx")
```

### Direct Arrow Access

```python
import pyarrow as pa

table = pa.table({"id": [1, 2, 3], "name": ["Alice", "Bob", "Carol"]})

ws.write_dataframe(0, 0, table, header_format=header_fmt)
```

**Current type support:** `int8`/`int16`/`int32`/`int64`, `uint8`/`uint16`/`uint32`/`uint64`, `float32`/`float64`, `string`/`utf8`, `large_utf8`, `utf8view` (Polars default), `bool`, `date32`/`date64`, and `timestamp` in all four units (`s`/`ms`/`us`/`ns` -- `ns` is pandas' default `datetime64[ns]`).

Date and timestamp columns are written as real Excel dates with a `yyyy-mm-dd` / `yyyy-mm-dd hh:mm:ss` number format applied automatically, rather than as raw serial numbers. Timezone-aware timestamps are written as UTC wall-clock time (Excel has no timezone concept) and emit a `UserWarning`; call `.dt.tz_convert(None)` first if you want to choose the offset yourself.

---

## Column, row and range formats

```python
from rvgsrust_xlsxwriter import Workbook, Format

wb = Workbook()
ws = wb.add_worksheet()

money = Format()
money.set_num_format("#,##0.00")

ws.set_column_format(2, money)              # whole column
ws.set_column_range_format(3, 5, money)     # columns D-F
ws.set_row_format(0, money)                 # whole row
ws.set_cell_format(0, 0, money)             # single cell
ws.set_range_format(1, 0, 10, 4, money)     # a rectangular range
```

Column and row formats apply to cells that don't carry a format of their
own, which makes them the way to reformat data written by
`write_dataframe`.

## Page setup and printing

```python
ws.set_landscape()
ws.set_paper_size(9)                                  # 9 = A4, 1 = Letter
ws.set_margins(0.5, 0.5, 0.75, 0.75, 0.3, 0.3)        # inches
ws.set_print_area(0, 0, 49, 7)
ws.set_repeat_rows(0, 0)                              # header row on every page
ws.set_print_fit_to_pages(1, 0)                       # 1 page wide, any height
ws.set_print_gridlines(True)
ws.set_print_center_horizontally(True)
ws.set_page_breaks([25, 50])
```

Also `set_portrait`, `set_page_order`, `set_repeat_columns`,
`set_print_scale`, `set_vertical_page_breaks`, `set_print_headings`,
`set_print_center_vertically`, `set_print_black_and_white`,
`set_print_draft` and `set_print_first_page_number`.

These set worksheet metadata rather than writing cells, so they are
unaffected by `constant_memory` row ordering even where they take row
numbers.

### Headers and footers

```python
ws.set_header("&CPage &[Page] of &[Pages]")
ws.set_footer("&LConfidential&RGenerated by rvgsrust-xlsxwriter")
```

`&L`, `&C`, `&R` align text to the left/center/right section.
`&[Page]`, `&[Pages]`, `&[File]`, `&[Tab]`, `&[Date]` are Excel
placeholders expanded when the file is opened. This library doesn't
validate the string, but it isn't a raw pass-through either:
`rust_xlsxwriter` normalizes the bracket syntax to the older
single-letter codes before writing (`&[Page]` becomes `&P`, `&[Pages]`
becomes `&N`, `&[Tab]` becomes `&A`) -- both spellings are valid Excel
codes and render identically once opened, the file on disk just won't
contain whichever bracket form you typed. Excel's 256-character limit
on the combined string (including control characters) isn't checked at
write time; an oversized string is silently truncated by Excel rather
than rejected. Full syntax reference:
https://rustxlsxwriter.github.io/worksheet/headers.html

## Conditional Formatting

Twelve rule types are supported. Build a rule object, then attach it to a
range with `Worksheet.add_conditional_format(first_row, first_col,
last_row, last_col, rule)`.

```python
from rvgsrust_xlsxwriter import (
    Workbook, Format, ConditionalFormatCell, ConditionalFormat3ColorScale,
    ConditionalFormatDataBar,
)

wb = Workbook()
ws = wb.add_worksheet()
for i, v in enumerate([10, 20, 30, 40, 50]):
    ws.write(i, 0, v)

bad = Format()
bad.set_background_color("#FFC7CE")

high = ConditionalFormatCell()
high.set_rule_greater_than(35)
high.set_format(bad)
ws.add_conditional_format(0, 0, 4, 0, high)

scale = ConditionalFormat3ColorScale()
scale.set_minimum_color("#F8696B")
scale.set_midpoint_color("#FFEB84")
scale.set_maximum_color("#63BE7B")
ws.add_conditional_format(0, 1, 4, 1, scale)

bar = ConditionalFormatDataBar()
bar.set_fill_color("#638EC6")
bar.set_direction("left_to_right")
ws.add_conditional_format(0, 2, 4, 2, bar)

wb.close("cf.xlsx")
```

| Class | Rule setters |
|---|---|
| `ConditionalFormatCell` | `set_rule_greater_than`, `..._less_than`, `..._equal_to`, `..._not_equal_to`, `..._greater_than_or_equal_to`, `..._less_than_or_equal_to`, `set_rule_between`, `set_rule_not_between` |
| `ConditionalFormatBlank` | `invert()` for non-blank |
| `ConditionalFormatDuplicate` | `invert()` for unique |
| `ConditionalFormatError` | `invert()` for non-error |
| `ConditionalFormatFormula` | `set_rule("=$A1>50")` |
| `ConditionalFormatAverage` | `set_rule("above" \| "below" \| "equal_or_above" \| "equal_or_below" \| "{1,2,3}_std_dev_above" \| "..._below")` |
| `ConditionalFormatTop` | `set_rule("top" \| "bottom" \| "top_percent" \| "bottom_percent", n)` |
| `ConditionalFormatText` | `set_rule("contains" \| "does_not_contain" \| "begins_with" \| "ends_with", text)` |
| `ConditionalFormatDate` | `set_rule("yesterday" \| "today" \| "tomorrow" \| "last_7_days" \| "last_week" \| "this_week" \| "next_week" \| "last_month" \| "this_month" \| "next_month")` |
| `ConditionalFormat2ColorScale` | `set_minimum/set_maximum(type, value)`, `set_minimum_color/set_maximum_color` |
| `ConditionalFormat3ColorScale` | as above plus `set_midpoint`, `set_midpoint_color` |
| `ConditionalFormatDataBar` | `set_minimum/set_maximum`, `set_fill_color`, `set_border_color`, `set_negative_fill_color`, `set_negative_border_color`, `set_axis_color`, `set_solid_fill`, `set_border_off`, `set_bar_only`, `set_direction`, `set_axis_position`, `use_classic_style` |

Rule-type strings for `set_minimum`/`set_maximum`/`set_midpoint` are
`automatic`, `lowest`/`min`, `highest`/`max`, `number`, `percent`,
`percentile`, `formula`. Values may be numbers, or strings when the type
is `formula`. Every class also has `set_multi_range(range)` and
`set_stop_if_true(enable)`.

Color scales and data bars have no `set_format()`: Excel renders those
from the scale or bar definition itself rather than from a format record.

Icon sets are not exposed yet.

## Sparklines

```python
from rvgsrust_xlsxwriter import Workbook, Sparkline

wb = Workbook()
ws = wb.add_worksheet()
for r in range(5):
    for c in range(5):
        ws.write(r, c, (r + 1) * (c + 1))

sp = Sparkline()
sp.set_range("Sheet1!A1:E1")
sp.set_type("column")
sp.show_high_point(True)
sp.set_sparkline_color("#638EC6")
ws.add_sparkline(0, 5, sp)

# A grouped sparkline shares one set of options across a range. Its data
# range must be 2D, one row per sparkline in the group.
group = Sparkline()
group.set_range("Sheet1!A1:E5")
group.set_group_max(True)
ws.add_sparkline_group(0, 6, 4, 6, group)

wb.close("sparklines.xlsx")
```

Types are `line` (default), `column`, and `win_lose`. Point markers:
`show_high_point`, `show_low_point`, `show_first_point`,
`show_last_point`, `show_negative_points`, `show_markers`, `show_axis`,
`show_hidden_data`. Colors: `set_sparkline_color`, plus
`set_high_point_color`, `set_low_point_color`, `set_first_point_color`,
`set_last_point_color`, `set_negative_points_color`, `set_markers_color`.
Scaling: `set_line_weight`, `set_custom_max`, `set_custom_min`,
`set_group_max`, `set_group_min`, `set_style`. Also `set_date_range`,
`set_right_to_left`, `set_column_order`, and `show_empty_cells_as` with
`gaps`, `zero` or `connected`.

## Charts

```python
from rvgsrust_xlsxwriter import Workbook, Chart, ChartSeries

wb = Workbook()
ws = wb.add_worksheet()
for i, (label, value) in enumerate([("Jan", 10), ("Feb", 40), ("Mar", 20)]):
    ws.write(i, 0, label)
    ws.write(i, 1, value)

series = ChartSeries()
series.set_categories("Sheet1!$A$1:$A$3")
series.set_values("Sheet1!$B$1:$B$3")
series.set_name("Revenue")

chart = Chart("column")
chart.push_series(series)
chart.set_title_name("Revenue by month")
chart.set_x_axis_name("Month")
chart.set_y_axis_name("USD")
chart.set_y_axis_min(0.0)
chart.set_legend_position("bottom")
ws.insert_chart(0, 3, chart, 10, 10)   # last two args are pixel offsets

wb.close("chart.xlsx")
```

The 23 chart types are `area`, `area_stacked`, `area_percent_stacked`,
`bar`, `bar_stacked`, `bar_percent_stacked`, `column`, `column_stacked`,
`column_percent_stacked`, `doughnut`, `line`, `line_stacked`,
`line_percent_stacked`, `pie`, `radar`, `radar_with_markers`,
`radar_filled`, `scatter`, `scatter_straight`,
`scatter_straight_with_markers`, `scatter_smooth`,
`scatter_smooth_with_markers`, `stock`.

Series are built separately and attached with `Chart.push_series()`.
`ChartSeries` supports `set_values`, `set_categories`, `set_name`,
`set_secondary_axis`, `set_overlap`, `set_gap`, `set_smooth`,
`set_invert_if_negative`, `set_invert_if_negative_color`,
`delete_from_legend` and `set_point_colors`.

Axes, titles and legends are **not** separate objects, because their
constructors are `pub(crate)` upstream. They are flattened onto `Chart` as
`set_title_name` / `set_title_hidden` / `set_title_overlay`,
`set_x_axis_*` and `set_y_axis_*` (`name`, `min`, `max`, `major_unit`,
`minor_unit`, `log_base`, `num_format`, `hidden`, `reverse`,
`major_gridlines`, `minor_gridlines`, plus `date_axis` / `text_axis` on x),
and `set_legend_position` / `set_legend_hidden` / `set_legend_overlay`.

Legend positions are `right`, `left`, `top`, `bottom`, `top_right`. There
is no overlay position; use `set_legend_overlay(True)` alongside one of
those.

Note that `set_title_hidden()`, `set_legend_hidden()`,
`set_x_axis_reverse()`, `set_y_axis_reverse()`, `show_hidden_data()` and
`show_na_as_empty_cell()` take no arguments, matching upstream.

Chart formatting (`ChartFormat`, `ChartFont`) and series decorations
(`ChartMarker`, `ChartTrendline`, `ChartDataLabel`) are not exposed yet.

### Chart formatting

`ChartFormat` handles fills, lines and borders; `ChartFont` handles text.

```python
from rvgsrust_xlsxwriter import Chart, ChartSeries, ChartFormat, ChartFont

bar_style = ChartFormat()
bar_style.set_fill_color("#4472C4")
bar_style.set_border_color("#000000")
bar_style.set_border_width(1.0)

title_font = ChartFont()
title_font.set_bold()
title_font.set_size(16.0)
title_font.set_color("#333333")

series = ChartSeries()
series.set_values("Sheet1!$B$1:$B$5")
series.set_format(bar_style)

chart = Chart("column")
chart.push_series(series)
chart.set_title_name("Styled")
chart.set_title_font(title_font)
```

`ChartLine` and `ChartSolidFill` are not separate classes. Upstream they
exist only to be handed to `ChartFormat`, so they are flattened into it:
`set_line_color`, `set_line_width`, `set_line_dash_type`,
`set_line_transparency`, `set_line_hidden`, `set_no_line`, the same six
under `set_border_*` / `set_no_border`, and `set_fill_color`,
`set_fill_transparency`, `set_no_fill`. Line state is kept per format
object, so successive calls compose.

Dash types are `solid`, `round_dot`, `square_dot`, `dash`, `dash_dot`,
`long_dash`, `long_dash_dot`, `long_dash_dot_dot`.

`ChartFont` supports `set_bold`, `unset_bold`, `set_default_bold`,
`set_italic`, `set_underline`, `set_strikethrough`, `set_color`,
`set_name`, `set_size`, `set_rotation`, `set_right_to_left`,
`set_pitch_family` and `set_character_set`. Note `set_bold`, `set_italic`,
`set_underline` and `set_strikethrough` take no arguments, matching
upstream.

Attachment points: `ChartSeries.set_format`, and on `Chart`
`set_title_font` / `set_title_format`, `set_x_axis_font` /
`set_x_axis_name_font` / `set_x_axis_format` / `set_x_axis_name_format`,
the same four for `y_axis`, and `set_legend_font` / `set_legend_format`.

Pattern and gradient fills are not exposed yet.

### Series markers, trendlines and data labels

```python
from rvgsrust_xlsxwriter import (
    Chart, ChartSeries, ChartMarker, ChartTrendline, ChartDataLabel,
)

series = ChartSeries()
series.set_values("Sheet1!$B$1:$B$5")

marker = ChartMarker()
marker.set_type("circle")
marker.set_size(7)
series.set_marker(marker)

trend = ChartTrendline()
trend.set_type("moving_average", 3)
trend.set_display_r_squared(True)
series.set_trendline(trend)

label = ChartDataLabel()
label.show_value()
label.set_position("above")
series.set_data_label(label)

chart = Chart("line")
chart.push_series(series)
```

Markers set on a series survive being pushed to a chart, including on
line, radar and scatter chart types where Excel defaults markers to off.
Those defaults still apply when no marker is set explicitly.

Marker types are `square`, `diamond`, `triangle`, `x`, `star`,
`short_dash`, `long_dash`, `circle`, `plus_sign`. Automatic and no-marker
are **not** types: use `set_automatic()` or `set_none()`.

Trendline types are `none`, `linear`, `exponential`, `logarithmic`
(not `logarithm`), `power`, `polynomial`, `moving_average`. The last two
take a period as the second argument to `set_type`, defaulting to 2.
`set_display_equation` and `set_display_r_squared` are named with a `set_`
prefix here for consistency, though upstream omits it.

Data labels support `show_value`, `show_category_name`,
`show_series_name`, `show_leader_lines`, `show_legend_key`,
`show_percentage`, `show_x_value`, `show_y_value`, `set_hidden`,
`set_position`, `set_num_format`, `set_separator`, `set_value`,
`set_font`, `set_format` and `set_custom`. Positions are `default`,
`center`, `right`, `left`, `above`, `below`, `inside_base`, `inside_end`,
`outside_end`, `best_fit`.

For per-point labels, build one `ChartDataLabel` per point, call
`set_custom()` on the ones that should differ, and pass the list to
`ChartSeries.set_custom_data_labels()`.

## Performance & Benchmarks

See [PERFORMANCE.md](PERFORMANCE.md) for full tables and methodology.

**100,000 rows × 8 columns (800,000 cells) — measured on Intel i5-6267U, Python 3.12, `rust_xlsxwriter` 0.96:**

| Method | Mean | vs xlsxwriter |
|---|---|---|
| **rvgs** `write_dataframe()` pandas | 1.43s | 🏆 **8.3× faster** |
| **rvgs** `write_dataframe()` polars | 1.53s | 7.8× faster |
| **rvgs** `write_dataframe()` pyarrow | 1.65s | 7.2× faster |
| **rvgs** `write_records()` (bulk dict) | 1.94s | 6.1× faster |
| **rvgs** `write_rows()` (list-of-lists) | 2.03s | 5.8× faster |
| xlsxwriter `write_row()` | 11.87s | 1× (baseline) |
| openpyxl write-only | 18.67s | 1.6× slower |
| pandas `to_excel()` openpyxl | 25.03s | 2.1× slower |

At 10,000 rows: **6.8× faster** than xlsxwriter. At 1,000 rows: **5.6× faster**.

The `write_dataframe()` path uses the Arrow C-stream zero-copy interface — pandas, polars, and pyarrow DataFrames all feed through this route with no Python-side data conversion.

**Run the benchmark on your machine:**

```bash
maturin develop --release
pip install xlsxwriter openpyxl pandas pyarrow polars
python benchmarks/run_benchmarks.py --runs 7 --warmup 2
```

---

## Roadmap

| Version | Features |
|---------|----------|
| **v0.1** | ✅ Core writing, formatting, merging, formulas, dates, images, Polars/Pandas support |
| **v0.2** | ✅ Bulk `write_records()` / `write_rows()`; Arrow zero-copy `write_dataframe()`; `constant_memory` streaming mode; autofilter; defined names; worksheet tables; charts (phases 1-3); conditional formatting; sparklines; page setup; extended Arrow types |
| **v0.2.1** | ✅ Audit release: correctness fixes, GIL released during `save()`, allocation-free Arrow string path, streamed `write_dataframe()`, cross-platform CI |
| **v0.2.2** | ✅ Patch release: `Workbook.close()` / `with Workbook(path) as wb:` now work with no argument, using the constructor-provided path; version metadata alignment; `set_column_range_width()`; canonical `set_border_top/bottom/left/right()` names (old names kept as aliases); `.pyi` type stubs + `py.typed` marker; `Cargo.lock` committed; `write_dataframe(column_formats=...)` -- a true per-cell merge, so a border survives on a date column alongside its own number format, not the column-scoped workaround this shipped with first; `annotations` no longer leaks into the module namespace |
| **v0.2.3** | ✅ Patch release: `Workbook.save_to_buffer() -> bytes`; `Worksheet.set_header()`/`set_footer()` (text only, images still need an `Image` pyclass); `write_rows()` no longer double-materializes the dataset before writing |
| **v0.3** | 🚧 Data validation (dropdown lists, cell rules); row/column outline grouping |
| **v0.4** | 🚧 Conditional format icon sets; chart error bars and secondary axes; cell notes and autofilter criteria; full xlsxwriter API compatibility layer |

**Charts** (`rust_xlsxwriter`'s largest subsystem -- 18k+ lines, 23 chart
types, ~214 public methods across ~39 types) were implemented in phases
so each could be verified rather than shipping a large surface untested.
All three phases have landed:
1. ✅ Core `Chart`/`ChartSeries` + common types (Bar/Column/Line/Pie/
   Scatter + stacked variants) + basic title/legend/axis
2. ✅ Formatting depth: `ChartFormat`/`ChartFont`, line/font/fill styling
3. ✅ Decorations: `ChartMarker`, `ChartTrendline`, `ChartDataLabel`

Remaining chart work, tracked in [MISSING.md](MISSING.md): error bars,
data tables, manual layout, and the less common chart types (Radar,
Stock, Doughnut, Surface).

### Known limitations

These are current, deliberate gaps rather than oversights:

- **`write_dataframe()` column types.** Decimal, list, struct and
  dictionary-encoded Arrow columns are not supported; such a column
  raises `TypeError` and `dataframe.py` falls back to the per-cell path.
- **Timezones.** Excel has no timezone concept. Timezone-aware Arrow
  timestamps are written as UTC wall-clock time and warn once per column.
- **Precision.** Excel cells hold an f64, so integers above 2^53 lose
  precision. This is a format limitation, not an implementation one.
- **`constant_memory=True`** requires rows to be written in
  non-decreasing order. This layer raises `ValueError` on violation;
  `rust_xlsxwriter` itself would silently emit a corrupt file.
- **Python 3.8.** `requires-python` still declares `>=3.8`, but CI tests
  3.9 upward; the `pandas>=2.0` extra already requires 3.9+.

---

## API Parity

[MISSING.md](MISSING.md) audits the exposed Python API against
rust_xlsxwriter 0.96 and lists what is not yet wrapped, with upstream
`file:line` references, a suggested Python API shape, and a priority for
each. Sparklines, cell/row/column/range formats, page setup and print
settings, and per-side border naming are all at full parity now -- an
earlier audit pass had flagged per-side borders as missing entirely,
which was a false positive from a naming mismatch, and separately the
naming itself (`set_top_border` vs upstream's `set_border_top`) has
since been reconciled: both spellings work, the reversed ones kept for
compatibility.

The largest remaining gaps are data validation and row/column outline
grouping on `Worksheet` (header/footer text images -- `set_header_image`/
`set_footer_image` -- still need an `Image` pyclass first); error bars,
secondary axes and a handful of formatting options on `Chart`; and icon
sets on conditional formats. `Format` has ~6 low-priority gaps left
(quote prefix, hyperlink style, checkbox format, font family/charset,
reading direction, `unset_*` inverses) -- see MISSING.md's "Suggested
order" for the full ranked list.

## Performance TODO

Measured bottlenecks and untried optimisations are logged in
[PERFORMANCE_TODO.md](PERFORMANCE_TODO.md), separately from the benchmark
results in [PERFORMANCE.md](PERFORMANCE.md). Nothing there is implemented
yet, by design: they change hot paths and each needs its own before/after
benchmark run.

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Commit your changes: `git commit -m 'Add amazing feature'`
4. Push to the branch: `git push origin feature/amazing-feature`
5. Open a Pull Request

---

## License

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

---

## Acknowledgements

- [John McNamara](https://github.com/jmcnamara) for `rust_xlsxwriter` and `XlsxWriter`
- [PyO3](https://github.com/PyO3/pyo3) for Rust-Python bindings
- [maturin](https://github.com/PyO3/maturin) for building and publishing

---

<p align="center">
  <b>RVGSRust — Excel at Rust speed, with Python ease.</b>
</p>

