Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
groups:
actions:
patterns: ["*"]
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Tests

on:
push:
branches: [main]
tags: ['v*']
paths-ignore: ['README*.md', 'CHANGELOG.md', 'LICENSE']
pull_request:
paths-ignore: ['README*.md', 'CHANGELOG.md', 'LICENSE']
workflow_dispatch:
permissions:
contents: read
# A newer push to a pull request replaces the run that is still going
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
plan:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.pick.outputs.matrix }}
steps:
- id: pick
shell: bash
# macOS and Windows run for a release tag or a manual run, Linux on every push
run: |
linux='{"os":"ubuntu-latest","python":"3.12"},{"os":"ubuntu-latest","python":"3.13"},{"os":"ubuntu-latest","python":"3.14"}'
if [[ "$GITHUB_REF" == refs/tags/* || "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then
echo "matrix={\"include\":[$linux,{\"os\":\"windows-latest\",\"python\":\"3.14\"},{\"os\":\"macos-latest\",\"python\":\"3.14\"}]}" >> "$GITHUB_OUTPUT"
else
echo "matrix={\"include\":[$linux]}" >> "$GITHUB_OUTPUT"
fi

test:
needs: plan
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.0
- run: uv run --locked --python ${{ matrix.python }} pytest -q
18 changes: 18 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: lint
on:
push:
branches: [main]
pull_request:
# A newer push to a pull request replaces the run that is still going
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.0
- run: uvx ruff@0.17.1 check
- run: uvx ruff@0.17.1 format --check
19 changes: 19 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Publish to PyPI

on:
push:
tags:
- 'v*'

jobs:
publish:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # PyPI Trusted Publishing
contents: read
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.0
- run: uv build
- uses: pypa/gh-action-pypi-publish@release/v1
13 changes: 13 additions & 0 deletions .github/workflows/readme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: readme
on:
push:
branches: [main]
pull_request:
# A newer push to a pull request replaces the run that is still going
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
readme:
uses: NaNIsaac/.github/.github/workflows/readme.yml@main
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
.venv
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## 0.1.0 (unreleased)

- `use`, `style`, `rc`: Origin-like `box` and `house` variants for paper, slide and poster.
- Colour, marker and dash cycle from cvdkit.
- `figure` sized for single and double journal columns, `panel_label`.
- IBM Plex Sans bundled for the `house` variant.
89 changes: 89 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# qoraplot

[English](README.md) | [한국어](README.ko.md)

matplotlib용 Origin 스타일 출판용 그림 스타일입니다. 네 면 모두 안쪽을 향한 눈금과 보조 눈금, 닫힌 테두리, 격자
없음, 산세리프 글꼴을 쓰고, 저널 단 폭·슬라이드·포스터 크기에 맞춥니다. 색·마커·점선 순환은
[cvdkit](https://github.com/NaNIsaac/cvdkit)에서 가져오므로 색각 이상이 있어도 계열이 구별됩니다.

qoraplot은 OriginLab과 관계가 없으며, "Origin 스타일"은 모양을 설명하는 말일 뿐입니다.

## 설치

Python 3.12 이상이 필요합니다. PyPI에 첫 릴리스가 올라가기 전까지는 이 저장소를 clone한 폴더에서
설치합니다.

<!-- test unix -->
```sh
uv venv
uv pip install .
```

<!-- test windows -->
```powershell
uv venv
uv pip install .
```

import되는지 확인합니다.

<!-- test unix -->
```sh
uv run --no-project python -c "import qoraplot; print(qoraplot.VARIANTS)"
```

<!-- test windows -->
```powershell
uv run --no-project python -c "import qoraplot; print(qoraplot.VARIANTS)"
```

## 사용법

```python
import matplotlib.pyplot as plt
import numpy as np
import qoraplot

qoraplot.use("box", "paper") # 변형 "box" 또는 "house"; 맥락 "paper", "slide", "poster"

fig, ax = qoraplot.figure("single") # 폭 3.5 in; "double"은 7.2 in
x = np.linspace(0, 10, 30)
for tau in (1, 2, 4):
ax.plot(x, np.exp(-x / tau), label=f"tau = {tau}")
ax.set_xlabel("time (ns)")
ax.legend()
qoraplot.panel_label(ax, "a")
fig.savefig("figure.pdf") # paper는 300 dpi, TrueType 글꼴 포함

with qoraplot.style("house", "poster"): # 블록 안에서만 적용; 저장도 블록 안에서
fig, ax = qoraplot.figure(14.4, aspect=0.5)
ax.plot(x, np.sin(x))
fig.savefig("poster_panel.png")
```

글꼴은 그림을 그릴 때 정해지므로 `style(...)`을 쓸 때는 `savefig`도 블록 안에서 호출합니다.

## 변형

같은 데이터로 그린 두 변형입니다. 아직 기본값이 없으므로 하나를 고르기 전까지는 호출마다 변형을 지정합니다.

- `box`: 닫힌 테두리, 네 면 안쪽 눈금, 보조 눈금, 격자 없음, Arial 또는 Helvetica(없으면 Liberation Sans,
그다음 DejaVu Sans), 검은 선.
- `house`: 열린 L자 테두리, 회색 축과 눈금, 거의 검은 글자, 옅은 격자, IBM Plex Sans(포함됨)와 STIX 수식,
안쪽 눈금.

`box`:

![box variant](docs/variant_box.png)

`house`:

![house variant](docs/variant_house.png)

녹색맹으로 본 두 변형(cvdkit 시뮬레이션): [docs/variants_deutan.png](docs/variants_deutan.png).
그림은 `examples/variants.py`로 그립니다.

## 라이선스

MIT. IBM Plex Sans는 SIL Open Font License 1.1로 포함되어 있습니다
([src/qoraplot/fonts/OFL.txt](src/qoraplot/fonts/OFL.txt)).
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# qoraplot

[English](README.md) | [한국어](README.ko.md)

An Origin-like publication style for matplotlib (`box`): ticks inside on all four sides with minor ticks,
a closed frame, no grid and a sans-serif face, sized for journal columns, slides and posters. The
colour, marker and dash cycle comes from [cvdkit](https://github.com/NaNIsaac/cvdkit), so series
stay apart with colour-vision deficiency.

qoraplot is not affiliated with OriginLab; "Origin-like" only describes the look.

## Install

qoraplot needs Python 3.12 or newer. Until the first release is on PyPI, install it from a clone of
this repository:

<!-- test unix -->
```sh
uv venv
uv pip install .
```

<!-- test windows -->
```powershell
uv venv
uv pip install .
```

Check that it imports:

<!-- test unix -->
```sh
uv run --no-project python -c "import qoraplot; print(qoraplot.VARIANTS)"
```

<!-- test windows -->
```powershell
uv run --no-project python -c "import qoraplot; print(qoraplot.VARIANTS)"
```

## Use

```python
import matplotlib.pyplot as plt
import numpy as np
import qoraplot

qoraplot.use("box", "paper") # variant "box" or "house"; context "paper", "slide" or "poster"

fig, ax = qoraplot.figure("single") # 3.5 in wide; "double" is 7.2 in
x = np.linspace(0, 10, 30)
for tau in (1, 2, 4):
ax.plot(x, np.exp(-x / tau), label=f"tau = {tau}")
ax.set_xlabel("time (ns)")
ax.legend()
qoraplot.panel_label(ax, "a")
fig.savefig("figure.pdf") # 300 dpi for paper, TrueType fonts embedded

with qoraplot.style("house", "poster"): # only inside the block; save inside it too
fig, ax = qoraplot.figure(14.4, aspect=0.5)
ax.plot(x, np.sin(x))
fig.savefig("poster_panel.png")
```

Fonts are looked up when a figure is drawn, so with `style(...)` call `savefig` inside the block.

## Variants

Two variants on the same data. There is no default yet: name the variant in every call until one
is chosen.

- `box`: closed frame, ticks inside on all four sides, minor ticks, no grid, Arial or Helvetica
(falling back to Liberation Sans, then DejaVu Sans), black ink.
- `house`: open L frame, grey axes and ticks, near-black text, light grid, IBM Plex Sans (bundled)
with STIX math, ticks inside.

`box`:

![box variant](docs/variant_box.png)

`house`:

![house variant](docs/variant_house.png)

Both as seen with deuteranopia (cvdkit simulation): [docs/variants_deutan.png](docs/variants_deutan.png).
The figures are drawn by `examples/variants.py`.

## License

MIT. IBM Plex Sans is bundled under the SIL Open Font License 1.1
([src/qoraplot/fonts/OFL.txt](src/qoraplot/fonts/OFL.txt)).
Binary file added docs/variant_box.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/variant_house.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/variants_deutan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions examples/variants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Draw the two style variants on the same data, for choosing the default.

Writes docs/variant_box.png, docs/variant_house.png and docs/variants_deutan.png (both variants as
seen with deuteranopia). Run: uv run python examples/variants.py
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import cvdkit
import matplotlib.pyplot as plt
import numpy as np

import qoraplot

DOCS = Path(__file__).resolve().parents[1] / "docs"


def data():
rng = np.random.default_rng(7)
t = np.linspace(0, 10, 40)
curves = {tau: np.exp(-t / tau) for tau in (1.0, 2.0, 4.0, 8.0)}
x = np.linspace(0.5, 9.5, 12)
err = 0.04 + 0.02 * rng.random(x.size)
y = 0.8 * np.exp(-x / 3.0) + 0.1 + rng.normal(0, err)
return t, curves, x, y, err


def draw(variant: str) -> np.ndarray:
"""Save the figure for `variant` and return its deuteranopia simulation."""
t, curves, x, y, err = data()
# Fonts are looked up when the figure is drawn, so saving stays inside the style block
with qoraplot.style(variant, "paper"):
fig, (a, b) = qoraplot.figure("double", aspect=0.36, ncols=2)
for tau, c in curves.items():
a.plot(t[::3], c[::3], label=rf"$\tau$ = {tau:g} ns")
a.set_xlabel("time $t$ (ns)")
a.set_ylabel("normalised intensity")
a.legend()
qoraplot.panel_label(a, "a")

b.errorbar(x, y, yerr=err, fmt="o", color="black", ms=3.5, label="measured")
xf = np.linspace(0, 10, 200)
b.plot(xf, 0.8 * np.exp(-xf / 3.0) + 0.1, marker="", label="fit, $\\tau$ = 3 ns")
b.set_xlabel("delay (ns)")
b.set_ylabel("signal (arb. units)")
b.legend()
qoraplot.panel_label(b, "b")
fig.savefig(DOCS / f"variant_{variant}.png", dpi=200)
return cvdkit.simulate(fig, "deutan")


def main() -> None:
DOCS.mkdir(exist_ok=True)
sims = [draw(variant) for variant in qoraplot.VARIANTS]
width = max(s.shape[1] for s in sims)
padded = [np.pad(s, ((0, 8), (0, width - s.shape[1]), (0, 0)), constant_values=1) for s in sims]
plt.imsave(DOCS / "variants_deutan.png", (np.vstack(padded) * 255).round().astype(np.uint8))


if __name__ == "__main__":
main()
Loading
Loading