diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2a8123b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish to PyPI + +on: + release: + types: [created] + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build backend + run: | + pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..24d76dd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Run tests / CI + +on: + push: + branches: [ main ] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install -e .[test] + pip install pytest pytest-cov + + - name: Run tests with coverage + run: | + pytest tests --cov=figctx --cov-report=term-missing --cov-fail-under=80 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b29106 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +.pytest_cache +figctx.egg-info +.venv +.coverage +.pytest_cache \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5ddf3a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# v0.1.0 - First Release + + +- Added features : single, multi, circuit +- Added categorical & sequential palettes + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..61fe9cf --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.md +recursive-include resource *.png *.svg \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c04d970 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Figctx - Figure Context for QuiME + +This Package helps you to conveniently make figures with QuiME template + +## How to install + + +```bash +pip install figctx +``` + +## Example1 (single figure context) + +```python +import numpy as np +import figctx as ctx + +testdata = np.random.rand(2,100) + +with ctx.single(80, 60, './example.png') as (fig,ax): + + ax.scatter(testdata[0], testdata[1], label='data') + ax.set_title('title') +``` + +![Example Single Figure](./resource/example.png) + + +## Example2 (multi figure context) + +```python +import numpy as np +import figctx as ctx + +testdata = np.random.rand(2,100) +testdata[0] = np.sort(testdata[0]) + +with ctx.multi(70, 150, 2, 1, [1], [1,2], './example2.png') as (fig, subfig): + + c = subfig[0] + + ax = c.subplots(1,1) + ax.tick_params( + direction='in', + width=1, + length=3, + pad=2 + ) + ax.plot(testdata[0], 2*testdata[1]+1, label='Test1') + + temp = np.random.rand(100) + ax.plot(testdata[0], temp, label='Test2') + + temp = np.random.rand(100) + ax.plot(testdata[0], 1.5*temp+0.5, label='Test3') + + ax.legend() + ax.set_title('ax title1') + + ax.set_xlabel('x') + ax.set_ylabel('y') + + c.suptitle('fig suptitle1') + + c = subfig[1] + + axes = c.subplots(1,2) + for ax in axes: + ax.tick_params( + direction='in', + width=1, + length=3, + pad=2 + ) + + ax = axes[0] + + ax.plot(testdata[1], testdata[0]) + ax.set_title('ax title2') + + ax = axes[1] + + ax.scatter(testdata[0], 0.5*testdata[1], label='Test3', color='black') + ax.scatter(2*testdata[1], 0.5*testdata[0], label='Test4') + ax.set_title('ax title3') + + ax.legend() + + c.suptitle('fig suptitle2') + + fig.suptitle('Suptitle') +``` + +![Example Multi Figure](./resource/example2.png) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4eb749c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel", "build"] +build-backend = "setuptools.build_meta" + + +[project] +name = "figctx" +version = "0.1.0" +description = "Figure context manager for QuiME Lab" +readme = "README.md" +requires-python = ">=3.8" +authors = [{name = "Lee Chan-yeong", email = "ckckdud123@gmail.com"}] +license = {text="MIT"} +dependencies = ["matplotlib >=3.3"] + + +[tool.setuptools.packages.find] +where = ["src"] + + +[project.optional-dependencies] +test = ["pytest", "pytest-cov"] + + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] +addopts = "--cov=figctx --cov-report=term-missing --cov-fail-under=80" + + +[tool.coverage.run] +source = ["src/figctx"] +branch = true + +[tool.coverage.report] +show_missing = true +skip_covered = true \ No newline at end of file diff --git a/resource/example.png b/resource/example.png new file mode 100644 index 0000000..50b02f7 Binary files /dev/null and b/resource/example.png differ diff --git a/resource/example2.png b/resource/example2.png new file mode 100644 index 0000000..52466ab Binary files /dev/null and b/resource/example2.png differ diff --git a/src/figctx/__init__.py b/src/figctx/__init__.py new file mode 100644 index 0000000..d1b3c67 --- /dev/null +++ b/src/figctx/__init__.py @@ -0,0 +1,5 @@ +from .context import single, multi, circuit + +__all__ = ['single', 'multi', 'circuit'] +__version__ = '1.0' +__license__ = 'MIT' diff --git a/src/figctx/context.py b/src/figctx/context.py new file mode 100644 index 0000000..d8ae6a6 --- /dev/null +++ b/src/figctx/context.py @@ -0,0 +1,127 @@ +import matplotlib.pyplot as plt +from .palette import * +from contextlib import contextmanager +from cycler import cycler + + +def mti(mm): + return mm / 25.4 + + +# single figure context manager +@contextmanager +def single(width_mm, height_mm, save_to, nrows=1, ncols=1): + + color_cycle = cycler(color=palette_categorical) + + custom_rc = { + 'font.family': 'Arial', + 'font.size': 7, + 'axes.prop_cycle': color_cycle, + 'axes.linewidth': 1, + 'lines.linewidth': 1.5, + 'lines.markersize': 3.5, + 'figure.dpi': 500, + 'savefig.dpi': 500, + } + + with plt.rc_context(rc=custom_rc): + fig, axes = plt.subplots( + nrows=nrows, + ncols=ncols, + figsize=(mti(width_mm), mti(height_mm)), + layout='constrained' + ) + + if isinstance(axes, (list, tuple, plt.Axes)): + axs = axes.ravel() if hasattr(axes, 'ravel') else [axes] + else: + axs = axes + + for ax in axs: + ax.tick_params( + direction='in', + width=1, + length=3, + pad=2 + ) + + if len(axs) == 1: + yield fig, axs[0] + else: + yield fig, axs + + fig.savefig(save_to) + plt.close(fig) + + +# multi figure context manager +@contextmanager +def multi( + width_mm, height_mm, + nrows, ncols, # (nrows, ncols) subfigure grid + width_ratios, height_ratios, + save_to, + wspace=0.08, + hspace=0.08, + constrained=True, +): + + color_cycle = cycler(color=palette_categorical) + + custom_rc = { + 'font.family': 'Arial', + 'font.size': 7, + 'axes.prop_cycle': color_cycle, + 'axes.linewidth': 1, + 'lines.linewidth': 1.5, + 'lines.markersize': 3.5, + 'figure.dpi': 500, + 'savefig.dpi': 500, + } + + with plt.rc_context(rc=custom_rc): + + fig = plt.figure(figsize=(mti(width_mm), mti(height_mm)) + , constrained_layout=constrained) + + subfigs = fig.subfigures(nrows, ncols, + wspace=wspace, hspace=hspace, + width_ratios=width_ratios, height_ratios=height_ratios) + + if nrows==1 and ncols==1: + subfigs = [subfigs] + + yield fig, subfigs + + fig.savefig(save_to) + plt.close(fig) + + +# Pennylane-fit circuit context +@contextmanager +def circuit(width_mm, height_mm): # pragma: no cover + + custom_rc = { + 'font.family': 'Arial', + 'font.size': 7, + 'axes.linewidth': 1, + 'lines.linewidth': 1.5, + 'figure.dpi': 500, + 'savefig.dpi': 500, + 'patch.facecolor': '#E4F1F7', + 'patch.edgecolor': '#0D4A70', + 'patch.linewidth': 1.5, + 'patch.force_edgecolor': True, + 'lines.color': '#0D4A70', + 'lines.linewidth': 1.5, + } + + figsize=(mti(width_mm), mti(height_mm)) + + with plt.rc_context(rc=custom_rc): + fig, _ = plt.subplots() + + yield fig, figsize + + plt.close(fig) \ No newline at end of file diff --git a/src/figctx/palette.py b/src/figctx/palette.py new file mode 100644 index 0000000..6cf0ca1 --- /dev/null +++ b/src/figctx/palette.py @@ -0,0 +1,72 @@ +from matplotlib.colors import ListedColormap, LinearSegmentedColormap + +palette_categorical = [ + '#FF1F5B', '#00CD6C', '#009ADE', + '#AF58BA', '#FFC61E', '#F28522', + '#A0B1BA', '#A6761D' +] + +palette_blue = [ + '#0D4A70', '#226E9C', '#3C93C2', + '#6CB0D6', '#9EC9E2', '#C5E1EF', + '#E4F1F7', +] + +palette_green = [ + '#003147', '#045275', '#00718B', + '#089099', '#46AEA0', '#7CCBA2', + '#B7E6A5' +] + +palette_grass = [ + '#06592A', '#228B3B', '#40AD5A', + '#6CBA7D', '#9CCEA7', '#CDE5D2', + '#E1F2E3' +] + +palette_warm = [ + '#6E005F', '#AB1866', '#D12959', + '#E05C5C', '#F08F6E', '#FABF78', + '#FCE1A4' +] + +palette_pink = [ + '#8F003B', '#C40F5B', '#E32977', + '#E95694', '#ED85B0', '#F2ACCA', + '#F9D8E6' +] + +palette_orange = [ + '#B10026', '#E31A1C', '#FC4E2A', + '#FDBD3C', '#FEB24C', '#FED976', + '#FFF3B2' +] + +# palette_test = [ +# '#003147', '#005A6D', '#008683', +# '#31B184', '#93D878', '#F9F871' +# ] + + +cmap_blue = ListedColormap(palette_blue, 'cmap_blue') +heatmap_blue = LinearSegmentedColormap.from_list('heatmap_blue', palette_blue) + + +cmap_green = ListedColormap(palette_green, 'cmap_green') +heatmap_green = LinearSegmentedColormap.from_list('heatmap_green', palette_green) + + +cmap_grass = ListedColormap(palette_grass, 'cmap_grass') +heatmap_grass = LinearSegmentedColormap.from_list('heatmap_grass', palette_grass) + + +cmap_warm = ListedColormap(palette_warm, 'cmap_warm') +heatmap_warm = LinearSegmentedColormap.from_list('heatmap_warm', palette_warm) + + +cmap_pink = ListedColormap(palette_pink, 'cmap_pink') +heatmap_pink = LinearSegmentedColormap.from_list('heatmap_pink', palette_pink) + + +cmap_orange = ListedColormap(palette_orange, 'cmap_orange') +heatmap_orange = LinearSegmentedColormap.from_list('heatmap_orange', palette_orange) diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..9e60243 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,99 @@ +import os +import numpy as np +import figctx as ctx +import pytest + +testdata = np.random.rand(2,100) + +def test(tmp_path): + + save_to1 = tmp_path / 'test1.png' + save_to2 = tmp_path / 'test2.png' + save_to3 = tmp_path / 'test3.png' + save_to4 = tmp_path / 'test4.png' + + with ctx.single(80, 60, save_to1) as (fig,ax): + + ax.plot(testdata[0], testdata[1]) + ax.set_title('ax title') + + fig.suptitle('fig title') + + with ctx.single(160, 60, save_to2, 2, 1) as (fig, ax): + + c = ax[0] + + c.errorbar(testdata[0], testdata[1], fmt='-o') + + c = ax[1] + + c.scatter(testdata[1], testdata[0], label='Testlabel') + + c.legend() + + with ctx.multi(70, 150, 1, 1, [1], [1], save_to3) as (fig, subfig): + + c = subfig[0] + + ax = c.subplots(1,1) + ax.tick_params( + direction='in', + width=1, + length=3, + pad=2 + ) + ax.plot(testdata[0], 2*testdata[1], label='Test1') + ax.legend() + ax.set_title('ax title1') + + with ctx.multi(70, 150, 2, 1, [1], [1,2], save_to4) as (fig, subfig): + + c = subfig[0] + + ax = c.subplots(1,1) + ax.tick_params( + direction='in', + width=1, + length=3, + pad=2 + ) + ax.plot(testdata[0], 2*testdata[1], label='Test1') + ax.legend() + ax.set_title('ax title1') + + c.suptitle('fig suptitle1') + + c = subfig[1] + + axes = c.subplots(1,2) + for ax in axes: + ax.tick_params( + direction='in', + width=1, + length=3, + pad=2 + ) + + ax = axes[0] + + ax.plot(testdata[1], testdata[0]) + ax.set_title('ax title2') + + ax = axes[1] + + ax.scatter(testdata[0], 0.5*testdata[1], label='Test3', color='black') + ax.scatter(2*testdata[1], 0.5*testdata[0], label='Test4') + ax.set_title('ax title3') + + ax.legend() + + c.suptitle('fig suptitle2') + + fig.suptitle('Suptitle') + + assert save_to1.exists() and save_to2.exists() and save_to3.exists() + + +if __name__ == '__main__': + import pytest + pytest.main() \ No newline at end of file