> ## Documentation Index
> Fetch the complete documentation index at: https://jetxl.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Monthly sales report

> Build a formatted single-sheet report from start to finish

A complete script producing a report you could send without editing: title block, formatted currency, a native table, data bars and a chart.

<Steps>
  <Step title="Prepare the data">
    Any Arrow-capable frame works. This uses Polars.

    ```python theme={null}
    import polars as pl
    import jetxl as jet

    df = pl.DataFrame({
        "Product":  ["Widget", "Gadget", "Doohickey", "Gizmo"],
        "Units":    [1200, 890, 2100, 450],
        "Price":    [19.99, 34.50, 8.75, 129.00],
        "Revenue":  [23988.00, 30705.00, 18375.00, 58050.00],
    })
    ```
  </Step>

  <Step title="Write the title block">
    `header_content` puts text above the data, and `data_start_row` moves the table down to make room.

    ```python theme={null}
    header = [
        (1, 0, "ACME Corporation"),
        (2, 0, "Monthly Sales Report"),
        (3, 0, "Generated 2026-01-15"),
    ]
    ```
  </Step>

  <Step title="Compose the call">
    ```python theme={null}
    jet.write_sheet_arrow(
        df.to_arrow(),
        "sales_report.xlsx",
        sheet_name="January",

        # title block
        header_content=header,
        data_start_row=5,
        merge_cells=[(1, 0, 1, 3), (2, 0, 2, 3)],

        # readability
        styled_headers=True,
        freeze_rows=5,
        auto_width=True,
        gridlines_visible=False,
        tab_color="0070C0",

        # numbers
        column_formats={
            "Units":   "thousands",
            "Price":   "currency",
            "Revenue": "currency",
        },

        # a native table over the data
        tables=[{
            "name": "SalesTable",
            "display_name": "January Sales",
            "start_row": 5,
            "start_col": 0,
            "style": "TableStyleMedium9",
            "show_row_stripes": True,
        }],

        # bars in the revenue column
        conditional_formats=[{
            "start_row": 6, "start_col": 3,
            "end_row": 6 + df.height - 1, "end_col": 3,
            "rule_type": "data_bar",
            "color": "638EC6",
            "show_value": True,
        }],

        # chart to the right
        charts=[{
            "chart_type": "column",
            "start_row": 5, "start_col": 0,
            "end_row": 5 + df.height, "end_col": 3,
            "from_col": 6,  "from_row": 5,
            "to_col": 14,   "to_row": 20,
            "title": "Revenue by Product",
            "title_bold": True,
            "title_font_size": 1400,
            "category_col": 0,
            "show_legend": False,
            "x_axis_title": "Product",
            "y_axis_title": "Revenue ($)",
            "axis_min": 0.0,
        }],
    )
    ```
  </Step>
</Steps>

<Note>
  This script is verified: it runs against jetxl 0.3.1 and produces a workbook that opens cleanly, with the title in A1, the column header on row 5 and the first data row on row 6.
</Note>

## Why the row numbers line up

The indexing rules differ by feature, which is the easiest thing to get wrong here.

| Setting                 | Value | Reason                                          |
| ----------------------- | ----- | ----------------------------------------------- |
| `data_start_row`        | `5`   | Rows 1 to 3 are the title, 4 is spacing         |
| `freeze_rows`           | `5`   | Everything through the header stays put         |
| Table `start_row`       | `5`   | The header row, 1-based                         |
| Conditional `start_row` | `6`   | First data row, below the header                |
| Chart `from_col`        | `6`   | Two clear columns right of the last data column |

<Tip>
  The table needs no `end_row` or `end_col`. Omitting them lets Jetxl size the range from the DataFrame, so the script keeps working when the data grows.
</Tip>

The conditional format and chart ranges use `df.height` rather than literals for the same reason. In Pandas, use `len(df)`.

<Note>
  Turning gridlines off does more for the look of a report than any other single setting. Combined with a native table, the result reads as designed rather than exported.
</Note>
