Metadata-Version: 2.4
Name: basic-svg-chart
Version: 0.2.2
Summary: Create basic SVG charts
Author: Guilhem Théron
License-Expression: AGPL-3.0-only
Project-URL: Homepage, https://framagit.org/iquyxa/basic-svg-chart
Project-URL: Source, https://framagit.org/iquyxa/basic-svg-chart
Project-URL: Documentation, https://framagit.org/iquyxa/basic-svg-chart#reference
Project-URL: Issues, https://framagit.org/iquyxa/basic-svg-chart/-/issues
Project-URL: Changelog, https://framagit.org/iquyxa/basic-svg-chart/-/blob/main/CHANGELOG.md
Keywords: svg,chart
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3 :: Only
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 :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.md
Dynamic: license-file

# basic-svg-chart

A simple Python library for creating basic [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG) charts,
dependency-free: it uses builtin [xml.etree.ElementTree](https://docs.python.org/3/library/xml.etree.elementtree.html).

To see what kind of charts can be created with this library, see [examples gallery](https://chart.gth.ovh/examples/).

![Line chart representing the annual global-average surface air temperature anomalies relative to 1850–1900 from 1940 to 2025 in ERA5 dataset, and a linear regression for the last 30 years](https://chart.gth.ovh/examples/era5_annual_light.svg)

This library can also be used as a [REST API](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/fastapi/main.py) (built with [FastAPI](https://fastapi.tiangolo.com/)). A html form to test this API is accessible [here](https://chart.gth.ovh/).

## Why another charting library?

Just because I didn't find a library that totally suits my needs, and after creating a few SVG charts manually with Calc formulas, I decided to lose tens (hundreds?) of hours to develop a new one. Yes, it is about that stupid.

More specifically, I wanted SVG charts with real text (that you can copy), real links (that you can click on) and some CSS interactions on hovering chart (greatly inspired by [Robbie Andrew awesome SVG charts](https://robbieandrew.github.io/)), whereas most SVG charting libraries don't handle these elements very well (text being transformed into shapes).

[SVG](https://en.wikipedia.org/wiki/SVG) has a lot of advantages over raster images: infinite scalability, small file size (even more when gzipped), support for interactivity and animation, selectable and searchable text, easy to use for lightly modifying an image (like changing a color, moving a shape, changing text...), etc.

[CSS](https://en.wikipedia.org/wiki/CSS) is a very powerful language that can be used for [for styling SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/SVG_and_CSS), which makes possible large customisation without the need to learn a new API and its hundreds of styling parameters. It also enables to adapt SVG charts rendering depending on device characteristics (like width for targeting small devices for instance) or user preferences (e.g. light or dark color theme) thanks to [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using). See sections [Customisation](#customisation) and [How-Tos](#how-tos) for examples on how to style charts using CSS.

You can find a lot of others SVG chart packages on [PyPI](https://pypi.org/search/?q=%22svg%22+chart) which maybe better suit you needs, for example:
- [pysvgchart](https://github.com/alex-rowley/py-svg-chart)
- [svg.charts](https://github.com/jaraco/svg.charts)
- [simplecharts](https://github.com/xi/simplecharts)
- [chart-builder](https://github.com/BrandynHamilton/Chart-Builder)

## When should I use it?

You probably should __NOT__ use this library at least in these cases:
- If you want a static image (either for using it in a `<img>` html tag or just to open it in a picture viewer), the css interactions, the links, the copyable text will be useless. This library aims to create web-first charts.
- For using the charts in a PDF file, a word processor or in a presentation program. They often handle badly css and the image will probably not render as in a web browser.
- Only line, scatter, vertical bar and box plot types are available for now, so of course if you want a horizontal bar, pie, radar, treemap, sankey... chart, don't use it.
- Customisation using dedicated parameters is limited, so if you want very fine customisation and don't want to use CSS you should probably prefer one of the above mentioned Python packages or tools like 🐍 [matplotlib](https://matplotlib.org/), 🇷 [ggplot2](https://ggplot2.tidyverse.org/), 📜 [Apache ECharts](https://echarts.apache.org/), 🧮 [LibreOffice Calc](https://www.libreoffice.org/)...
- Lots of data points to draw:
    * For line charts with more than 100,000 points, result might be slow to display and svg file will be large. See example [era5_moving_averages.svg](https://gth.ovh/charts/examples/era5_moving_averages.svg): about 120,000 data points, 2MB svg file (600kB gzipped). Or [test_large_dataset.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/tests/test_large_dataset.py) which creates a 600,000 points line chart.
    * For scatter charts, more than 10,000 data points can cause slowing. See example [meteo.svg](https://gth.ovh/charts/examples/meteo.svg): 8760 circles, 800kB svg file (100kB gzipped).
- Accessible chart: only a very few of all recommended [SVG accessibility guidelines](https://www.w3.org/TR/SVG-access/) are implemented for now.

## 🚧 WIP 🚧

> [!warning]
> ⚠️ It's still beta version, API is not stable yet and will probably break in the future!

## Installation

With [`pip`](https://pip.pypa.io/en/stable/):
```bash
python3 -m pip install basic-svg-chart
```

As it does not have any dependency, you can also just download [basic_svg_chart.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/src/basic_svg_chart/basic_svg_chart.py) and import it as a simple python module.

## Usage

```python
import basic_svg_chart

# Create a SVG line chart and store it in a string
my_line_chart = basic_svg_chart.chart(
    chart_type = "line", # optional, "line" is the default value
    x = range(3),
    y = [[10, 20, 30], [15, 8, 22], [None, 15, 5]],
    width = 800, height = 500, # optional, default values: width = 1200, height = 800
)

# Add to html file
html_page = f"""<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" ><title>Chart example</title></head><body><h1>Line chart</h1><figure id="chart">
{my_line_chart}
'<figcaption>A <a href="https://developer.mozilla.org/en-US/docs/Web/SVG">SVG</a> chart</figcaption></figure><p>Made with Python.</p></body></html>"""

with open("/tmp/chart.html", "w") as outputfile:
    outputfile.write(html_page)

# Write a scatter chart to a file
basic_svg_chart.scatter_chart(
    x = range(2020, 2026),
    y = (100, 102.6, 103.7, 101.03, 99.9),
    y_ticks_interval = 10,
    title = "Example scatter chart",
    legend_position = None,
    fullscreen = True,
    output = "/tmp/chart.svg")
```

See [real examples](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples) and [simple tests examples](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/tests/examples) for more.

## Reference

This library has one `chart()` function with 5 aliases `line_chart()`, `scatter_chart()`, `scatter_line_chart()`, `bar_chart()` and `box_plot_chart()` with fixed chart type.

> __basic_svg_chart__.__chart__(__x__, __y__, __xy__=None, __chart_type__="line", __bar_param__={'group_padding': 0.8, 'bar_padding': 1, 'group': "x", 'stack': False}, __box_plot_param__={'calculate_quantiles': False, 'whiskers': ("min", "max"), 'padding': 0.5, 'mean': False, 'outliers': False}, __names__=None, __title__=None, __desc__=None, __lang__=None, __colors__=None, __width__=1200, __height__=800, __fullscreen__=False, __margin__="auto", __x_unit__=None, __y_unit__=None, __x_ticks__=6, __x_axis_discrete__=True, __x_ticks_margin__=None, __x_ticks_position__="middle", __y_ticks__=6, __y_ticks_interval__=None, __y_min__=0, __y_max__=None, __big_mark__=None, __decimal_mark__=None, __x_ticks_format__="{:n}", __y_ticks_format__="{:n}", __show_values__=None, __values_position__="top", __values_position_shift__=None, __values_format__="{y}{y_unit}", __show_only_hovered_value__=True, __tooltip_type__=("title", "value"), __title_tooltip_format__=DEFAULT_TOOLTIP, __extra_data__=None, __circles__=4, __extra_lines__=None, __legend_position__="right", __legend_background__=None, __css__=None, __notes__=None, __font_size__=15, __background_color__="auto", __dark_mode__="user", __pretty__=False, __output__="string")

Create a SVG chart

### Parameters:

x : Sequence[int | float | str]
: The x values.

y : Sequence[Sequence[int | float | None]] | Sequence[int | float | None]
: The y values. If multiple series must be drawn, must be a sequence of sequences.
If some series has missing values, they have to be specified with None. If series sequence length < len(x), the last values will be missing.

xy: Sequence[Sequence[Sequence[int | float | str]]], optional
: If `x` and `y` are not provided, `xy` must contain the data points expressed as a sequence of series,
where each series is a sequence of points where each point is a sequence of 2 elements: x and y as (x, y).

chart_type : str | Sequence[str], optional, default "line"
: The type of the chart, can be "line", "scatter", "scatter line", "bar" or "box plot".
If a sequence is provided and there are multiple series, each series will have the specified type (based on sequence position, same as `y`).

bar_param: dict, optional, default {'group_padding': 0.8, 'bar_padding': 1, 'group': "x", 'stack': False}
: Specific parameters for bar charts.
    * `bar_param["group_padding"]` (default 0.8) is a float between 0 and 1 defining the space used by the group of `<rect>` elements / the maximum available space between 2 groups of `<rect>`s.
    * `bar_param["bar_padding"]` (default 1) is a float between 0 and 1 defining the space used by each `<rect>` / the maximum available space between 2 `<rect>`s.
    * `bar_param["group"]` defines how bars are grouped, with 2 possibles values:
"x" (the default) for grouping by x values (like line charts): first bar (on the left) is x1 for series1, then x1 for series2...
"series" for grouping by series: first bar (on the left) is x1 for series1 then x2 for series1...
    * `bar_param["stack"]` (default False) defines if bars must be stacked for each x. When bars are stacked, the first series (as defined in `y` sequence order) will be the closest to 0, and then stack follow series order.

box_plot_param: dict, optional, default {'calculate_quantiles': False, 'whiskers': ("min", "max"), 'padding': 0.5, 'mean': False, 'outliers': False}
: Specific parameters for box plot charts.
    * `box_plot_param["calculate_quantiles"]` (default False): if True, `y` values will be converted to [lower whisker, Q1, Q2, Q3, upper whisker]. Builtin method [statistics.quantiles](https://docs.python.org/3/library/statistics.html#statistics.quantiles) is used to calculate the values.
    * `box_plot_param["whiskers"]` (default ["min", "max"]): if `box_plot_param["calculate_quantiles"]` is True, define the metrics used for whiskers. Can be either "IQR" ([Q1-1.5*IQR, Q3-1.5*IQR], see https://en.wikipedia.org/wiki/Box_plot#Whiskers) or a couple (lower, upper) where lower and upper can be "min", "max" or a percentile as "Pn".
    * `box_plot_param["padding"]` (default 0.5): Define the width of the box as a percentage of the maximum possible width.
    * `box_plot_param["mean"]` (default False): if True, the mean will be added (and calculated if box_plot_param["calculate_quantiles"]) and represented as a diamond.
    * `box_plot_param["outliers"]` (default False): if True and `box_plot_param["calculate_quantiles"]` is True, the outliers will be added and represented as circles.

names : Sequence[str] | str, optional
: Names of a the series. The position in the sequence must match the position of the series in y sequence.
If not provided (or less names than the number of series defined in y), defaults to "Data series n" (with `n` incremental) or to `title` if there is just one series.

title : str, optional
: The optional title of the chart. If provided, will be printed as a `<text>` element above the plotting area + a `<title>` element.

desc : str
: The optional description of the chart, used in a `<desc>` element which is referenced by the `<svg>` `aria-describedby` attribute.

lang : str, optional
: A [BCP 47 language tag](https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag) used in `lang` and `xml:lang` attributes added to the root `<svg>` element.
If "fr" then `big_mark` parameter defaults to " " and `decimal_mark` to ",".

colors : Sequence[str], optional
: The colors of the series. Can be a [color name](https://www.w3schools.com/tags/ref_colornames.asp) (e.g. "green"), a [hex code](https://www.w3schools.com/colors/colors_hexadecimal.asp) (e.g. "#e91585") or a [RGB triplet](https://developer.mozilla.org/fr/docs/Web/CSS/Reference/Values/color_value/rgb) (e.g. "rgb(0, 191, 255)").
If not provided (or less colors than the number of series defined in y), defaults to [Okabe-Ito palette](https://siegal.bio.nyu.edu/color-palette) if 8 series or less, and [Viridis palette](https://waldyrious.net/viridis-palette-generator) if 9 ≤ len(series) ≤ 20.
If more than 20 series in `y`, `colors` must be provided.

width : int | float, optional, default 1200
: The width of the `<svg>` element. Used in a `viewbox` attribute (`viewBox="0 0 {width} {height}"`).
Will also be used in a `<svg>` `width` attribute if `fullscreen` = False.

height : int | float, optional, default 800
: The height of the `<svg>` element. Used in a `viewbox` attribute (`viewBox="0 0 {width} {height}"`)
Will also be used in a `<svg>` `height` attribute if `fullscreen` = False.

fullscreen : bool, optional, default False
: If False (the default), the root `<svg>` element will have `width` and `height` attributes as specified in the eponymous parameters.

margin: str | Sequence[int | float], optional, default "auto"
: Define the margins around the chart area.
Defaults to "auto" which try to calculate « optimal » margins depending on all elements of the chart (title, legend, units, notes...).
Otherwise, must be a sequence of 4 numbers which define [top, right, bottom, left] margins (in this order) in px.

x_unit : str, optional
: An optional unit for x values. If specified, will be drawn below the x axis (and centered).
Can also be used in `x_ticks_format`, `values_format` and `title_tooltip_format` parameters as "{x_unit}".

y_unit : str, optional
: An optional unit for y values. If specified, will be drawn above the y axis.
Can also be used in `y_ticks_format`, `values_format` and `title_tooltip_format` parameters as "{y_unit}".

x_ticks : int | Sequence[int | float | str], optional, default 6
: The ticks drawn in the x axis. Can be either an integer representing the number of ticks to show (see `x_axis_discrete`),
or a sequence of values.

x_axis_discrete : bool, optional, default True
: If True and `x_ticks` is an integer, the x ticks values will be pulled from `x` values, which can lead to an irregular scale.
If False and `x_ticks` is an integer, the x ticks will be equally distributed from min(x) to max(x), even if the ticks are not included in `x` values, leading to a regular scale.

x_ticks_margin: int | float, optional, default None
: The margin between the x ticks and the left and right sides of the chart area.
Defaults to None, which means:
    * Line or scatter charts: 0;
    * Bar charts: the space between the left side of the first rectangle and the left side of the chart area;
    * Box plot charts: 1.5 * the space between the left side of the first rectangle and the left side of the chart area.

x_ticks_position: str, optional, default "middle"
: The position of the x ticks: either "middle" (the default) for having ticks centered (same as the tick label) or "external" for having ticks around the tick label.
All other values will remove the x ticks (but keeping the vertical dotted lines and labels).

y_ticks : int | Sequence[int | float], optional, default 6
: The ticks drawn in the y axis. Can be either an int representing the number of ticks to show (not used if `y_ticks_interval` provided),
or a sequence of values.

y_ticks_interval : int | float, optional
: If `y_ticks` is not a sequence, `y_ticks_interval` defines the fixed interval between each tick, from `y_min` to `y_max`.

y_min : int | float | None, optional, default 0
: The min value drawn on the y ticks. If None (and `y_ticks` is not a sequence), will be defined as min(y).

y_max : int | float, optional
: The max value drawn on the y ticks. If not provided and `y_ticks` not provided as a sequence, will be defined as max(y), or will be computed using `y_ticks_interval` if provided.

big_mark : str, optional
: Can be used to replace default "\_" big mark separator when some numbers are formatted with "{:\_}" in `x_ticks_format`, `y_ticks_format`, `values_format` or `title_tooltip_format`.
In such a case, dont use "\_" as literal text in these formats as it will also be replaced by `big_mark`.

decimal_mark : str, optional
: Can be used to replace default "." decimal mark when some numbers are formatted with "{:.1f}" in `x_ticks_format`, `y_ticks_format`, `values_format` or `title_tooltip_format`.
In such a case, dont use "." as literal text in these formats as it will also be replaced by `decimal_mark`.

x_ticks_format : str, optional, default "{:n}"
: The [string format](https://docs.python.org/3/library/string.html#format-string-syntax) used to format x ticks values. "{x}" (with formatting options) will be replaced by the tick value and "{x_unit}" by the corresponding parameter.

y_ticks_format : str, optionall, default "{:n}"
: The [string format](https://docs.python.org/3/library/string.html#format-string-syntax) used to format y ticks values. "{y}" (with formatting options) will be replaced by the tick value and "{y_unit}" by the corresponding parameter.

show_values : str | Sequence[int | float | str], optional
: If provided, the specified values will be drawn on the chart (as `<text>` elements), next to the corresponding data points.
Can be either "all" for displaying all values, "last" for displaying only last values, or a sequence of x values for displaying only the corresponding y values.

values_position : str, optional, default "top"
: Where the value will be drawn relatively to the corresponding data point when `show_values` is provided or `tooltip_type` contains "value".
Can be one of "top", "right", "bottom", "left", "fixed above chart" and "fixed under chart".

values_position_shift : Sequence[int | float], optional
: The specific shift in px defined as (x, y) relatively to the data points where to draw values when `show_values` is provided or `tooltip_type` contains "value".
Defaults to (0, -12) when values_position="top", (0, 20) when "bottom", (12, 0) when "right" and (-12, 0) when "left".

values_format : str | Sequence[str], optional, default "{y}{y_unit}"
: The [string format](https://docs.python.org/3/library/string.html#format-string-syntax) used to format values when `show_values` is provided or `tooltip_type` contains "value".
"{y}" will be replaced by the y value, "{x}" by the x value, "{name}" by the series name, "{extra}" by the corresponding `extra_data` for the point/series hovered, and "{x_unit}" and "{y_unit}" by the corresponding parameters.

show_only_hovered_value : bool, optional, default True
: If True and `tooltip_type` contains "value" or "vertical line" and (`show_values` is defined or `chart_type` contains "scatter"), when hovering a specific value then all other values for the same series will be hidden and other circles will have reduced opacity.

tooltip_type : str | Sequence[str] | None, optional, default "value"
: The type of tooltip to display when hovering a data point. Can be a sequence containing "title", "value", "vertical line" or None to display no tooltip.
    * "value" will use `<text>` elements hidden by default and shown when hovering.
    * "title" will use `<title>` elements inside each `<circle>` elements representing the data points. These `<title>` elements are not rendered in mobile browsers.
    * "vertical line" will show a vertical line and all y values when hovering a x virtual vertical line. It doesn't work well if lots of x values.

title_tooltip_format : str | Sequence[str], optional, default "{name}: x = {x}{x_unit}, y = {y}{y_unit}"
: The [string format](https://docs.python.org/3/library/string.html#format-string-syntax) used to format tooltip when `tooltip_type` contains "title".
"{y}" will be replaced by the y value, "{x}" by the x value, "{name}" by the series name, "{extra}" by the corresponding `extra_data` for the point/series hovered, and "{x_unit}" and "{y_unit}" by the corresponding parameters.
If a sequence is provided and there are multiple series, each series will have the specified tooltip format on hover (based on sequence position, same as `y`).

extra_data : Sequence[Sequence[int | float | str]] | Sequence[int | float | str], optional
: Extra data that can be used in `title_tooltip_format` and `values_format`.
Must be the same type as `y`: each data point will match the specified extra data based on sequence (of sequence) position.

circles : int | float, optional, default 4
: The `<circle>` radius in px (as `r` attribute).
Used when `chart_type` contains "scatter", or `show values` is provided, or `tooltip_type` is not None.

extra_lines : Sequence[dict] | dict, optional
: Extra horizontal or vertical lines to draw on the chart.
Each line is defined by a dictionary containing between 1 and 4 keys (multiple lines can be drawn using a sequence of dictionaries):
    * `'x'` for a vertical line or `'y'` for a horizontal line, the value representing where to draw the line in the x or y axis. Must be the same type as `x` and `y`, multiple lines are drawn is a sequence if provided.
    * `'color'` (optional, default "black"): the color of the line, as a color name (e.g. "green"), a hex code (e.g. "#e91585") or a RGB triplet (e.g. "rgb(0, 191, 255)").
    * `'thickness'` (optional, default 2): the [`stroke-width`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-width) attribute of the `<line>` element.
    * `'position'` (optional, default "back"): whether the lines are drawn behind ("back") or in front of ("front") the plots.

legend_position : str | None, optional, default "right"
: Where to draw the legend (containing series names).
To draw the legend outside the plotting area, use one of "top", "right", "bottom", "left".
To draw the legend inside the plotting area, use one of "top left", "top right", "bottom left", "bottom right".
None if legend must not be displayed.

legend_background : str, optional
: If provided, the background color of the legend (no background by default), as a color name (e.g. "green"), a hex code (e.g. "#e91585") or a RGB triplet (e.g. "rgb(0, 191, 255)").

css : Sequence[str] | str, optional
: The optional css declarations to add to the `<style>` element.

notes : str | Sequence[str], optional
: The optional notes to display as a `<text>` element above the chart.
Can contain svg elements (such as `<a>` for example) and raw text.
If a note starts with "`<i>`", text will be displayed in italic. If it starts with "`<b>`", text will be displayed in bold.
If a sequence is provided, each element will be treated as a new line, using `<tspan dy="1.5em">`.

font_size : int | float, optional, default 15
: The font size (in px) of the root `<svg>` element.

background_color : str | None, optional, default "auto"
: If not None, draw a `<rect>` filled with the specified color behind the chart.
Can be a color name (e.g. "green"), a hex code (e.g. "#e91585"), or a RGB triplet (e.g. "rgb(0, 191, 255)").
Defaults to "auto", which translates to "black" if `dark_mode="force"`, otherwise "white".
If `dark_mode="user"`, the background `<rect>` will be hidden if the browser is configured in dark color scheme. 

dark_mode : str | None, optional, default "user"
: If "user", add a few css properties to handle color scheme preference of user on web browser.
If "force", display the chart with a black background (instead of white in light mode) and white texts and axis lines (instead of black in light mode).
Otherwise, use light mode (white background + black text and axis lines).

pretty : bool, optional, default False
: If True, the `<svg>` output will be prettified using [xml.etree.ElementTree.indent()](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.indent) function.
If False, the `<svg>` output will be minified (fits in one line).

output : str, optional, default "string"
: If "string", the function will return the `<svg>` content as a string.
If "etree", the function will return the `<svg>` content as a xml.etree.ElementTree.Element.
Otherwise, will be interpreted as the name (absolute or relative path) of the svg file to create containing the chart.
In such a case, the function will also return the xml.etree.ElementTree.Element.

### Returns:

str or [xml.etree.ElementTree.Element](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element)
: SVG chart, either as a string if `output="string"` or a xml.etree.ElementTree.Element otherwise.
If `output` is the name (absolute or relative) of a file, the SVG content is written to that file
and the function returns the xml.etree.ElementTree.Element.

## Customisation

There are several ways to cutomise the charts created with this library.

### CSS

You can simply use the `css` parameter and add unlimited CSS declarations to the SVG chart. CSS is a very powerful language which can do a lot [for styling SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/SVG_and_CSS).
Your custom css declarations will be added at the end of the `<style>` element so that they take [precedence](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascade/Specificity) other the [default declarations](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/src/basic_svg_chart/basic_svg_chart.py#L49) added by the library, in case you want to override some.

For example, if you want a 20px italic title, Liberation Mono font, green x axis, blinking y unit, no vertical dotted gridlines, and move legend, you can use a `css` parameter like:

```python
import basic_svg_chart as bsc

bsc.scatter_chart(
    x = range(11),
    y = (range(11), [5] * 8),
    title = "CSS customisation example",
    y_unit = "€uros",
    css = (
        "text.title {font-size: 20px; font-style: italic;}",
        ":root {font-family: 'Liberation Mono';}",
        "#abscissa_lines > line {stroke: green;}",
        "#abscissa_text {fill: green;}",
        "#ordinate > text.unit {animation: blinker 1s linear infinite;}",
        "@keyframes blinker {50% {opacity: 0;}}",
        "#vertical_dotted_lines > line[stroke-dasharray] {display: none;}",
        ".legend {transform: translate(-300px, 500px);}"
    ),
    output = "/tmp/chart.svg"
)
```
See also [Examples](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples), most have a few custom css declarations. The [How-Tos](#how-tos) section gives also a few simple CSS tricks.

You can find a complete CSS guide on [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS).

### Programmatically

By using the `output="etree"` parameter, the function will return the <svg> chart content as a [xml.etree.ElementTree.Element](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element). Then you can modify whatever you want before converting the chart to a string (using [tostring()](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring)) or writing it to a file (using [write()](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.ElementTree.write)).

See also [Examples](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples), some are programmatically modified (like [carte.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/carte.py), [pop.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/pop.py), [ips.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/ips.py), [secten.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/secten.py)).

### Manually

[SVG](https://developer.mozilla.org/en-US/docs/Web/SVG) is quite a powerful and easy-to-understand format.
So you can also manually modify the svg generated file with any text editor for adding, removing, changing some elements for one-time adjustments. In such a case, you should probably use `pretty=True` parameter (or prettify the svg content with your text editor).

### With a vector graphics editor?

The svg generated file can be graphically modified using a vector graphics editor, like [Inkscape](https://inkscape.org/).

⚠️ Note that SVG editors don't necessarily handle well CSS, so you can have issues with that.
For instance, with default `dark_mode` parameter, Inkscape seems not to show the chart. You may have to comment (or remove) the line `:root {color-scheme: light dark;}` in the `<style>` element before opening the file with Inkscape. And after the file has been modified in Inkscape, it seems some CSS declarations are slightly modified and don't work anymore (like `circle` replaced by `.circle`), so you probably should be careful with that too.

## Compatibility

Tested on Linux Mint 22, with Python 3.10 to 3.14, and debian 13 + python 3.13. It probably works on other OS too, but it has not been tested.

Note that some examples rely on Linux platform, a few scripts must be changed to work on Windows or MacOS, like specifying `encoding="utf-8"` when reading csv files or changing the function to open the svg file after creation using `subprocess.call(["start", out_file], shell=True)` (see at the bottom of [run_examples.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/run_examples.py#L120)).

Chart render is not exactly the same on different web browsers, it has only been tested on Firefox and Chromium.

## Old python versions

> [!warning]
> This old python version is not maintened!

This library requires Python ≥ 3.10 to work. If you are using 3.6 ≤ Python < 3.10, you can use the file [old/basic_svg_chart.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/old/basic_svg_chart.py) (version 0.1.0), which is compatible with Python ≥ 3.6 but with a few differences/limitations:
- [`typing.Union`](https://docs.python.org/3/library/typing.html#typing.Union) instead of `|` which was [introduced in Python 3.10](https://docs.python.org/3/whatsnew/3.10.html#pep-604-new-type-union-operator);
- [`isinstance()`](https://docs.python.org/3/library/functions.html#isinstance): classinfo can be a Union Type only since Python 3.10;
- Function annotations: replace `list` ([introduced in Python 3.9](https://docs.python.org/3.9/whatsnew/3.9.html#type-hinting-generics-in-standard-collections)) by `typing.List`;
- Parameter `pretty=True` won't work as it uses `ElementTree.indent()` which was [introduced in Python 3.9](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.indent).

You can see the changes made for version 0.1.0 in this [commit](https://framagit.org/iquyxa/basic-svg-chart/-/commit/369b9c37).

For Python < 3.6, it won't work at least because of heavy use of [f-strings](https://docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals).

## How-Tos

<details>
<summary>Handle y-axis scale with negative values</summary>

The are several parameters for configuring y axis: `y_ticks` (default 6), `y_ticks_interval`, `y_min` (default 0) and `y_max`. An important point is that when your dataset contains positive and negative values, `y_min` must be specified as it defaults to 0, either by fixing it to None (in which case y_min will be computed as min(y)) or the desired negative value.
Example:
```python
import basic_svg_chart

basic_svg_chart.line_chart(
    x = range(10),
    y = ([4, 4, -2, 0, -4, -3, -2, -1, -3, 1], range(10), [5] * 7),
    y_min = -5, # or y_min = None which is equivalent to y_min = 4
    y_ticks_interval = 1,
    output = "/tmp/chart.svg"
)
```
See examples [inflation.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/inflation.py), [era5](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/era5.py) or [roni](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/roni.py).
</details>

<details>
<summary>Change margins around the chart (add extra space on the right because legend is truncated for instance)</summary>

The parameter `margin` (which must be a sequence of four numbers defined as [top, right, bottom, left]) can be used to change default margins. If you first want to know what are the default calculated margins of you chart, you have to enable [logging](https://docs.python.org/3/library/logging.html) first.
Example:
```python
import logging
import basic_svg_chart

logging.basicConfig(level = logging.INFO, format = "[%(levelname)s] - %(message)s")

basic_svg_chart.line_chart(
    x = range(10),
    y = range(10),
    names = "TOO LARGE NAME 😭",
    output = "/tmp/chart.svg"
)

# Log will output:
# [INFO] - margin: (27, 190, 34.50, 47)

basic_svg_chart.line_chart(
    x = range(10),
    y = range(10),
    names = "TOO LARGE NAME 😀",
    margin = (27, 230, 34.50, 47), # for adding 40px to the right margin
    output = "/tmp/chart.svg"
)
```
</details>

<details>
<summary>Remove horizontal and/or vertical dotted tick lines</summary>

You can hide the default dotted tick lines with css:
```python
import basic_svg_chart

basic_svg_chart.line_chart(
    x = range(10),
    y = range(10),
    css = (
        "#horizontal_dotted_lines > line[stroke-dasharray] {display: none;}", # hide only horizontal dotted lines
        "#vertical_dotted_lines > line[stroke-dasharray] {display: none;}", # hide only vertical dotted lines
        # "line[stroke-dasharray] {display: none;}", # hide both in a single CSS declaration
    ),
    output = "/tmp/chart.svg"
)
```
</details>

<details>
<summary>Move some elements (legend, ticks labels, values...)</summary>

Use css property [transform](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transform) (or more directly the more recent [translate](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/translate) property) to move some elements of the chart.
For example, if you want to move y unit to the left, x tick labels to the top and change legend position:
```python
import basic_svg_chart

basic_svg_chart.line_chart(
    x = range(2020, 2025),
    y = ([100, 102, 101.4, 103.8, 104.8], [100, 100.3, 99.7, 99.2, 99.5]),
    names = ("Increasing index", "Decreasing index"),
    y_unit = "2020=100",
    y_ticks = 12,
    legend_position = "top",
    css = (
        "g#ordinate > text.unit {transform: translate(-20px);}", # Move y unit to the left
        "#abscissa_text {transform: translate(0, -4px);}", # Move x tick labels to the top
        "#legend1 {translate: 500px 50px}", # Move series 1 legend (same as `{transform: translate(500px, 50px)`)
        "#legend2 {translate: 700px 110px}", # Move series 2 legend
        ".legend > line {display: none;}", # Optionally, hide legend symbols
    ),
    margin = (30, 20, 30, 50), # reduce top margin because legend has been moved
    output = "/tmp/chart.svg"
)
```
See also examples: [nvidia.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/nvidia.py) (move y unit to the left and x ticks values to the top), [roni.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/roni.py) (move specific values shown on chart), [death.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/death.py) (move mean value for box plot), [electricite.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/electricite.py) (move x ticks labels to put them between 2 ticks), [energy.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/energy.py) (manually move last values text elements to display them in the middle of the corresponding bar), [heatmap.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/heatmap.py) (move legend text elements next to the corresponding series).
</details>

<details>
<summary>Rotate text elements by 90° (y unit, x ticks labels...)</summary>

Use css property [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/writing-mode) to change text direction.
For example, if you want to put y unit on the left of the ordinate axis with text rotated by 90°, and rotate x tick labels in the reverse direction:
```python
import basic_svg_chart
import random

y = [100]
y = [y := y + [y[-1] + random.random() - 0.5] for i in range(99)][-1]

basic_svg_chart.line_chart(
    x = range(1900, 2000),
    x_ticks = 21,
    y = y,
    y_unit = "Index (1900=100)",
    tooltip_type = None,
    css = (
        "g#ordinate > .unit {writing-mode: sideways-lr; transform: translate(-25px, 300px);}",
        "g#abscissa_text > text {writing-mode: vertical-rl;}",
    ),
    margin = (20, 20, 60, 60),
    legend_position = "bottom right",
    output = "/tmp/chart.svg"
)
```
See example [life_satisfaction.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/life_satisfaction.py).
</details>

<details>
<summary>Lower opacity of other bars of the same series on hover</summary>

Use css property [opacity](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/opacity) to lower opacity of some specific elements.
Example:
```python
import basic_svg_chart

basic_svg_chart.bar_chart(
    x = range(10),
    y = (range(10), [-i for i in range(1, 11)], [4] * 5 + [5] * 2 + [None] + [3]),
    y_ticks = [-10 + 5*i for i in range(5)],
    bar_param = {'group': 'series', 'group_padding': 0.9, 'bar_padding': 0.9},
    css = (
        "g.series:hover > rect {opacity: 0.1;}"
        "g#series_plots > g.series > rect:hover {opacity: 1;}",
    ),
    values_position = "fixed above chart",
    values_format = "{name}: ({x}, {y})",
    legend_position = None,
    fullscreen = True,
    output = "/tmp/chart.svg"
)
```
See example [rain.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/rain.py).
</details>

<details>
<summary>Change font family</summary>

Use css property [font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-family). Note that text height and width differ depending on font family, so you may have to move some svg elements, change margin or text size to keep the chart readable.
Example:
```python
import basic_svg_chart
import random

basic_svg_chart.scatter_chart(
    x = range(10),
    y = [random.random() for i in range(10)],
    y_ticks = [i/10 for i in range(11)],
    title = "Scatter chart with cursive font family",
    css = "svg {font-family: cursive, system-ui;}",
    output = "/tmp/chart.svg"
)
```
See example [pop.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/pop.py).
</details>

<details>
<summary>Change title (or other text element) font size</summary>

Use css property [font-size](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-size) to lower or increse font-size of text elements.
Example:
```python
import basic_svg_chart

basic_svg_chart.chart(
    chart_type = "scatter line",
    x = [f"01/{i:02}" for i in range(1, 13)],
    y = [10, 11, 12, 13, 14, 15, 15, 14, 13, 12, 11, 10],
    title = "Scatter line chart with a long title too large to fit in the image with default font-size",
    legend_position = None,
    css = "text.title {font-size: 1em;}", # default title font-size = 1.4em
    width = 800, height = 500,
    output = "/tmp/chart.svg"
)
```
See example [era5_annual.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/era5_annual.py).
</details>

<details>
<summary>Use a dark theme</summary>

By default charts are displayed with a dark theme for users requesting a dark color scheme in their browser (and with a light theme for users preferring a light color scheme). To change this behaviour, use parameter `dark_mode`: `"force"` will show dark theme charts for all users, `None` will show light theme charts for all users. To change some styles for dark (or light) theme, use [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-color-scheme) CSS media feature.

For example, changing several colors for dark theme users can be done like that:
```python
import basic_svg_chart

basic_svg_chart.chart(
    x = range(10),
    y = [range(10), [(i + 1)/2 if i % 2 == 0 else -i/3 for i in range(10)]],
    y_ticks = [-5, 0, 5, 10],
    title = "Customizing dark theme",
    notes = "Using <a href='https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-color-scheme' target='_blank'>prefers-color-scheme</a> CSS media feature.",
    colors = ("orange", "black"),
    css = """
    @media (prefers-color-scheme: dark) {
        .color2 {stroke: DeepSkyBlue;}
        .color2 text {fill: DeepSkyBlue;}
        text.title, text#notes {fill: greenyellow;}
        line[stroke-dasharray] {stroke: red;}
        a {fill: hotpink;}
    }""",
    output = "/tmp/chart.svg"
)
```
See example [energy.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/energy.py).
</details>

<details>
<summary>Use a specific locale</summary>

if you want to use `{:n}` format with a specific [locale](https://docs.python.org/3/library/locale.html), you have to set it first with `locale.setlocale(...)`.
Example:

```python
import basic_svg_chart
import locale
print(locale.getlocale())
# locale.setlocale(locale.LC_ALL, "")
locale.setlocale(locale.LC_ALL, "fr_FR.utf8")

basic_svg_chart.line_chart(x = [x/100 for x in range(11)], y = range(0, 10001, 1000), output = "/tmp/chart.svg")
```
</details>

## Other limitations

Probably a lot, but here are some:
- Dates cannot be used in x, you have to convert them to strings (see examples [era5_moving_averages.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/era5_moving_averages.py), [meteo.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/meteo.py), [electricite.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/electricite.py), [roni.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/roni.py), [heatmat.py](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/examples/heatmap.py));
- If you use "<", ">" or "&" in a text, it must be replaced by [html entities](https://www.w3schools.com/html/html_entities.asp): "&amp;lt;", "&amp;gt;", "&amp;amp;";
- Most texts are left aligned (notes, legend, units), so it isn't adapted to right-to-left writing systems (and using [`direction` attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/direction) will do [weird things](https://stackoverflow.com/questions/42120707/svg-text-writing-direction));
- [Other known issues](https://framagit.org/iquyxa/basic-svg-chart/-/tree/main/TODO.md#bug-fixes).

