trace-digitiser 2.0.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. trace_digitiser-2.2.0/CHANGELOG.md +228 -0
  2. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/MANIFEST.in +2 -0
  3. {trace_digitiser-2.0.0/src/trace_digitiser.egg-info → trace_digitiser-2.2.0}/PKG-INFO +73 -13
  4. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/README.md +70 -10
  5. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/RELEASING.md +25 -11
  6. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/pyproject.toml +6 -2
  7. trace_digitiser-2.2.0/src/trace_digitiser/__init__.py +362 -0
  8. trace_digitiser-2.2.0/src/trace_digitiser/_figure.py +480 -0
  9. trace_digitiser-2.2.0/src/trace_digitiser/_results.py +240 -0
  10. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/axis_labels.py +98 -27
  11. trace_digitiser-2.2.0/src/trace_digitiser/bars.py +591 -0
  12. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/calibration.py +9 -3
  13. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/cli.py +246 -0
  14. trace_digitiser-2.2.0/src/trace_digitiser/explore.py +193 -0
  15. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/frames.py +16 -5
  16. trace_digitiser-2.2.0/src/trace_digitiser/heatmap.py +368 -0
  17. trace_digitiser-2.2.0/src/trace_digitiser/markers.py +587 -0
  18. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/ocr.py +22 -2
  19. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/panel_detection.py +110 -3
  20. trace_digitiser-2.2.0/src/trace_digitiser/points.py +213 -0
  21. trace_digitiser-2.2.0/src/trace_digitiser/polar.py +327 -0
  22. trace_digitiser-2.2.0/src/trace_digitiser/rows.py +313 -0
  23. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/trace_detection.py +28 -4
  24. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/x_calibration.py +1 -2
  25. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0/src/trace_digitiser.egg-info}/PKG-INFO +73 -13
  26. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser.egg-info/SOURCES.txt +11 -0
  27. trace_digitiser-2.2.0/tests/test_figure_styles.py +138 -0
  28. trace_digitiser-2.2.0/tests/test_readers.py +417 -0
  29. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_trace_detection.py +4 -1
  30. trace_digitiser-2.0.0/CHANGELOG.md +0 -109
  31. trace_digitiser-2.0.0/src/trace_digitiser/__init__.py +0 -625
  32. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/LICENSE +0 -0
  33. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/examples/README.md +0 -0
  34. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/examples/digitise_published_figure.py +0 -0
  35. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/examples/lopatina2014_fig4.jpg +0 -0
  36. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/examples/validate_with_known_data.py +0 -0
  37. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/examples/walkthrough.ipynb +0 -0
  38. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/setup.cfg +0 -0
  39. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/diagnostics.py +0 -0
  40. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/digitise.py +0 -0
  41. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/geometry.py +0 -0
  42. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/io.py +0 -0
  43. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/line_detection.py +0 -0
  44. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/models.py +0 -0
  45. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/provenance.py +0 -0
  46. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/resampling.py +0 -0
  47. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/straighten.py +0 -0
  48. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/summarise.py +0 -0
  49. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/synthetic.py +0 -0
  50. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser/validation.py +0 -0
  51. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser.egg-info/dependency_links.txt +0 -0
  52. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser.egg-info/entry_points.txt +0 -0
  53. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser.egg-info/requires.txt +0 -0
  54. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/src/trace_digitiser.egg-info/top_level.txt +0 -0
  55. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/__init__.py +0 -0
  56. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/conftest.py +0 -0
  57. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/data/lopatina2014_fig4_reference.csv +0 -0
  58. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_axis_labels.py +0 -0
  59. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_calibration.py +0 -0
  60. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_cli.py +0 -0
  61. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_digitise.py +0 -0
  62. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_frames.py +0 -0
  63. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_geometry.py +0 -0
  64. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_integration.py +0 -0
  65. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_io.py +0 -0
  66. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_panel_detection.py +0 -0
  67. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_published_figure.py +0 -0
  68. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_straighten.py +0 -0
  69. {trace_digitiser-2.0.0 → trace_digitiser-2.2.0}/tests/test_validation.py +0 -0
@@ -0,0 +1,228 @@
1
+ # Changelog
2
+
3
+ All notable changes to trace-digitiser are recorded here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
+ and the project uses [semantic versioning](https://semver.org/).
6
+
7
+ ## [2.2.0] - 2026-11-15
8
+
9
+ Tested on the figures of published papers whose underlying numbers are
10
+ published too (Leypunskiy et al. 2017, eLife 6:e23539: markers with error
11
+ bars, a line over shaded bands; Foley et al. 2019, eLife 8:e50063: bar charts;
12
+ Pearl and Reed 1920, PNAS 6:275, a microfilm scan), 2.1.0 read what it found
13
+ accurately but often found nothing. This release finds much more of it, there
14
+ and on the figures of a paper held out until the changes were made (Andreani
15
+ et al. 2022, eLife 11:e74327). Line plots digitise as before: the package's
16
+ validation suite and the Hearn and Whitmore (2026) examples give the same
17
+ results.
18
+
19
+ ### Added
20
+
21
+ - **A test set of published figures**
22
+ ([`validation/published-figures`](https://github.com/comparativechrono/trace-digitiser/tree/main/validation/published-figures)):
23
+ seven figures from those four papers, the numbers behind them from their
24
+ authors' data, and a script that reads them with nothing given and with the
25
+ help a user can give, and compares. Its README has the results of 2.1.0 and
26
+ 2.2.0, case by case, including the few that 2.2.0 reads worse. It is run
27
+ before each release and is not part of the package.
28
+
29
+ ### Fixed
30
+
31
+ - **Bar charts in the style of GraphPad Prism**: black, grey, hatched and
32
+ outlined bars are found, on an axis that does not start at 0 (2.1.0 found 3
33
+ of the 35 bars of Foley et al.'s figure 2, and read those upside down; all
34
+ 35 are now read, within 0.3 % of the axis). Bars are the columns filled from
35
+ the baseline, so an error bar or a significance bracket joined to a bar no
36
+ longer spoils it; a border of another colour (a black outline round a grey
37
+ bar) is part of the bar; a bar cut by the edge of the plot area is kept.
38
+ - **Error bars drawn in the colour of their bar or marker** (the default in
39
+ Prism, MATLAB and matplotlib) are followed beyond the bar's end, or from the
40
+ marker, to their caps; one running across a fitted line goes on beyond it.
41
+ Where only the half beyond a bar's end shows, a note says so.
42
+ - **Markers sitting on a line of their own colour** (a fitted line, their
43
+ error bars) are found: other sizes are tried where the lines' ridges
44
+ outnumber the markers, and such a colour counts as a colour of markers if
45
+ its markers are joined to its lines and much thicker than them. Round
46
+ letters of words written in the plot, and a legend's markers beside their
47
+ words, are left out; a marker found in two series of close colours is kept
48
+ in the nearer one.
49
+ - **A plot area given by hand** (a box drawn in the workbench, or numbers read
50
+ off a picture) moves to the middle of the axis lines within a few pixels of
51
+ it, and their thickness is measured: with a thick axis line the tick labels
52
+ were looked for inside it, and the axis title was read instead.
53
+ - Long tick marks of a small plot in a large picture are found, so that its
54
+ labels are anchored to them.
55
+ - A colour that fills areas (a shaded band behind a plot) is not taken for a
56
+ trace of a line plot.
57
+ - Tick labels that Tesseract misreads as a line of text (75 as 15, in large
58
+ Helvetica) are read once more as a single word when an axis is not settled.
59
+ - Looking at a whole page of a scanned paper (`describe_figure`, the
60
+ workbench's first look at a page of a PDF) took a minute and a half, nearly
61
+ all of it in the search for polar axes; the circles are now proposed on a
62
+ smaller copy of the page (16 seconds in all).
63
+
64
+ ## [2.1.0] - 2026-11-14
65
+
66
+ Everything in 2.0.0 works as before. What is new is readers for other kinds of
67
+ figure, and helpers for graphical tools such as the
68
+ [digitiser workbench](https://github.com/comparativechrono/digitiser-workbench).
69
+
70
+ ### Added
71
+
72
+ - **Points**: `digitise_points` (`trace-digitiser points`) reads the markers of
73
+ a scatter or dot plot, or of a line drawn with markers, whatever their
74
+ shape, separating markers that overlap; and the error bars on them,
75
+ vertical or horizontal, to the middle of their caps.
76
+ - **Bars**: `digitise_bars` (`trace-digitiser bars`) reads where each bar
77
+ ends, upright or lying down, rising or hanging from the baseline, and its
78
+ error bar; a legend's patch is left out.
79
+ - **Rows**: `digitise_rows` (`trace-digitiser rows`) reads figures drawn as a
80
+ stack of rows - actograms, double-plotted records, birthograms - bin by bin,
81
+ and the dots that mark a peak in them.
82
+ - **Heat maps**: `digitise_heatmap` (`trace-digitiser heatmap`) reads each
83
+ cell's value from its colour on the colour bar, whose labels are read like
84
+ an axis's (or from a matplotlib colour map).
85
+ - **Polar plots**: `digitise_polar` (`trace-digitiser polar`) finds the polar
86
+ axes and their rings, and reads each point's angle and distance.
87
+ - All of them return a `FigureResult` and write a CSV file, a provenance file
88
+ and, if asked, an overlay, as `digitise` does.
89
+ - `describe_figure` (`trace-digitiser describe`): what the automatic steps
90
+ find in a figure - plot areas, each axis's tick labels and readings, the
91
+ colours drawn, a colour bar, polar axes - as plain values.
92
+ - `pixels_to_values`: points picked by hand, in the figure's units.
93
+ - Tick labels are read on the right-hand side of a plot too (a colour bar, a
94
+ second y axis): `calibrate_axis(..., "y2")`.
95
+ - `y_axis="right"` (`--y-axis right`) reads a line's values against a second
96
+ y axis on the right of the plot (a twin axis); with `trace_colours`, the
97
+ lines drawn against each axis are digitised in a run of their own.
98
+ - `describe_figure` gives the tick marks found on each plot's axes, for a
99
+ program that lets you click on a tick.
100
+ - Colours can be given by name (`"black"`, `"steelblue"`, `"tab:blue"`).
101
+ - Plots stacked on a shared x axis, of which only the lowest has labels, take
102
+ its x scale (with a message saying so).
103
+ - A plot area hidden among others in a grid (a heat map whose dark colours
104
+ hide its axis lines) is found by its frame.
105
+
106
+ ### Changed
107
+
108
+ - An axis scale is held to medium confidence when the tick labels support a
109
+ different scale just as well. Tesseract compiled to WebAssembly (as it runs
110
+ in a browser) read the superscript minus signs of a log axis's exponents
111
+ differently from the Tesseract program, and two scales then fitted equally;
112
+ between log scales that tie, the one that steps by a decade is chosen.
113
+ - The sides of filled areas (bars) no longer make a plot area of their own
114
+ inside the real one, which bar charts with a legend could cause.
115
+ - A long axis (ten or more labels) whose labels nearly all agree is not read
116
+ again in the slower ways; fitting the labels of a long axis (the 59 years of
117
+ a birthogram) takes a second or two rather than half a minute, with the same
118
+ result.
119
+ - A heat map's colours are looked up on the colour bar once each, rather than
120
+ once for each pixel: Figure 4 of Hearn and Whitmore (2026) takes 4 seconds
121
+ rather than 45, with the same values.
122
+ - pytesseract is imported only when it is first used.
123
+
124
+ ## [2.0.0] - 2026-11-14
125
+
126
+ Every call and command-line option of 1.2.0 still works, with the same
127
+ meaning. The version number changes because the **results** change: figures
128
+ are analysed in a new way, and the values that come out differ from those of
129
+ 1.2.0. By the package's own tests against known data they are much closer to
130
+ what was drawn (55 of 57 test traces recovered to within 2 % with nothing
131
+ supplied, against 20; see the README). Anything digitised with 1.x that
132
+ matters should be digitised again.
133
+
134
+ ### Added
135
+
136
+ - **Plot areas are found without being told the layout.** Axis lines are
137
+ located to a fraction of a pixel and assembled into plot areas: any number
138
+ of panels, boxed or L-shaped axes, with or without gridlines and tick
139
+ marks. `layout_mode`, `expected_rows` and the rest remain as hints and
140
+ checks.
141
+ - **Tilted scans are straightened.** The tilt of the figure is measured and
142
+ undone first (`straighten="auto"`, `--straighten`), so a page that lay
143
+ askew on the scanner is digitised as accurately as one that did not.
144
+ - **Each automatic step can be replaced by the answer.** `plot_area` for the
145
+ position of the axes, `y_ref` / `x_ref` for reference points on an axis,
146
+ `y_scale` / `x_scale` for logarithmic axes, `trace_colours` for the
147
+ colours to follow, `exclude` for legends and annotations, `achromatic` for
148
+ black and grey traces, `ocr=False` to do without Tesseract.
149
+ - **The x axis can be calibrated** from `x_min` / `x_max`, from reference
150
+ points, or from numeric tick labels (`x_value` in the output).
151
+ - **Axis scales read from tick labels come with a confidence rating**, and a
152
+ scale that too few labels agree on is declined rather than guessed. The
153
+ rating, and the labels the scale rests on, are in the panel metadata.
154
+ - **Messages about anything that needs checking**: `result.warnings`,
155
+ `DigitiserWarning`, and `CHECK:` lines on the command line.
156
+ - **An overlay image** of the result drawn over the figure (`save_overlay`,
157
+ `--overlay`, `result.overlay()`): the quickest way to see whether a
158
+ digitisation is right.
159
+ - **A provenance file** for every run: version, SHA-256 of the image, all
160
+ settings, what was detected, and all messages.
161
+ - **Validation tools** (`trace_digitiser.validation`): `round_trip` tests the
162
+ digitiser on your own data drawn at the size of your figure;
163
+ `run_validation` (`--validate`) and `run_axis_validation` test it on 44
164
+ figures and 750 axes drawn from known data.
165
+ - New columns in the trace file: `n_pixels`, `interpolated`, `x_value`. New
166
+ columns at the end of the panel metadata: the sub-pixel plot area and how
167
+ each axis was calibrated. The 1.2.0 columns keep their names and order.
168
+ - Image arrays are accepted as well as file names; transparent images are
169
+ placed on white, and photographs are turned according to their EXIF data.
170
+ - `--version`; exit status 1 if any image could not be processed.
171
+ - Examples: two scripts, a notebook, and a published figure (CC BY) that is
172
+ also used in the tests.
173
+ - The repository itself: continuous testing on Python 3.10 to 3.14, and a
174
+ release workflow that publishes to PyPI through trusted publishing (see
175
+ `RELEASING.md`).
176
+
177
+ ### Changed
178
+
179
+ - **Values differ from 1.2.0**, chiefly because:
180
+ - the plot area is now bounded by the centres of the axis lines; 1.2.0 put
181
+ its edges up to 13 pixels outside them when tick marks pointed outwards,
182
+ which shifted and rescaled every value read with `y_min` / `y_max`;
183
+ - traces are followed down onto the axis lines when their colour allows,
184
+ and thin, pale, grey and black traces are no longer missed;
185
+ - gridlines, text and compression fringes are no longer reported as
186
+ extra traces (1.2.0 did so in about half of single-trace test figures).
187
+ - Axis labels are read in a new way (located geometrically, read by
188
+ Tesseract in several ways, and fitted on the principle that tick values
189
+ step by exactly the same amount). On the test axes it never gave a wrong
190
+ scale with high confidence; 1.2.0 misread decimal and logarithmic axes by
191
+ large factors without warning. It takes about half a second a figure
192
+ rather than twenty.
193
+ - Tesseract is now optional: without it, give the axis limits.
194
+ - Labels under 8 pixels high are read but never trusted outright; the
195
+ result carries a message asking for the values to be checked.
196
+ - `mean_estimate` and `center_estimate` are documented for what they are
197
+ (statistics of the trace's pixels in a column), with the consequence for
198
+ dense records spelt out in the README.
199
+
200
+ ### Fixed
201
+
202
+ - A one-pixel shift in the trace mask (from a 2×2 morphological opening)
203
+ that erased one-pixel lines and produced all-white "traces".
204
+ - `tests/conftest.py` was missing from the 1.2.0 source distribution, so its
205
+ tests could not be run from it.
206
+ - `use_lab=True` needed scikit-learn, and one helper needed SciPy, without
207
+ either being declared. scikit-learn is now the optional extra
208
+ `trace-digitiser[lab]`; SciPy is no longer used.
209
+ - `eval` of text read back from a data frame replaced by `ast.literal_eval`.
210
+ - The trace-accuracy test that failed in 1.2.0 (RMSE 102 against a limit of
211
+ 50) passes, and a new one holds all eight example figures to 7.5.
212
+
213
+ ### Removed
214
+
215
+ - The slow "exhaustive" reading of y labels is no longer tried by default
216
+ (`calibrate_y(..., exhaustive_fallback=True)` still does it). On axes that
217
+ the new method declined, it was right in none of twenty trials.
218
+
219
+ ## [1.2.0] - 2026-06-07
220
+
221
+ First version tracked in this repository; identical to the 1.2.0 files on PyPI.
222
+ Releases 0.1.0 to 1.0.0 (6-7 June 2026) were uploaded to PyPI directly and are
223
+ not recorded here.
224
+
225
+ [2.2.0]: https://github.com/comparativechrono/trace-digitiser/compare/v2.1.0...v2.2.0
226
+ [2.1.0]: https://github.com/comparativechrono/trace-digitiser/compare/v2.0.0...v2.1.0
227
+ [2.0.0]: https://github.com/comparativechrono/trace-digitiser/compare/1d34f75...v2.0.0
228
+ [1.2.0]: https://pypi.org/project/trace-digitiser/1.2.0/
@@ -5,3 +5,5 @@ recursive-include tests *.py *.csv
5
5
  recursive-include examples *.py *.md *.jpg *.ipynb
6
6
  prune examples/output
7
7
  include CHANGELOG.md RELEASING.md
8
+ # The published-figure test set (validation/published-figures) is run from the repository, not shipped.
9
+ prune validation
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trace-digitiser
3
- Version: 2.0.0
4
- Summary: Recover the data from line plots in figure images, on your own computer: no uploads, no AI model.
3
+ Version: 2.2.0
4
+ Summary: Recover the data from figure images - lines, points, bars, actograms, heat maps - on your own computer: no uploads, no AI model.
5
5
  Author: Trace Digitiser Contributors
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/comparativechrono/trace-digitiser
8
8
  Project-URL: Repository, https://github.com/comparativechrono/trace-digitiser
9
9
  Project-URL: Issues, https://github.com/comparativechrono/trace-digitiser/issues
10
10
  Project-URL: Changelog, https://github.com/comparativechrono/trace-digitiser/blob/main/CHANGELOG.md
11
- Keywords: plot digitizer,graph digitiser,data extraction,data rescue,computer vision,time series,circadian,actigraphy
11
+ Keywords: plot digitizer,graph digitiser,data extraction,data rescue,computer vision,time series,circadian,actigraphy,actogram,heat map,scatter plot,bar chart
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Science/Research
14
14
  Classifier: Operating System :: OS Independent
@@ -40,11 +40,12 @@ Dynamic: license-file
40
40
 
41
41
  # trace-digitiser
42
42
 
43
- Get the numbers back out of a line plot when the figure is all that survives — on your own computer.
43
+ Get the numbers back out of a figure when the figure is all that survives — on your own computer.
44
44
 
45
45
  `trace-digitiser` takes an image of a figure (a scan of an old paper, a figure saved from a PDF),
46
- finds the plot area, follows each trace, and writes the values to a CSV file. It is ordinary image
47
- processing, written to be read and checked.
46
+ finds the plot area, follows each trace, and writes the values to a CSV file. It reads line plots,
47
+ and also [points, bars, actograms, heat maps and polar plots](#other-kinds-of-figure). It is
48
+ ordinary image processing, written to be read and checked.
48
49
 
49
50
  **Nothing is uploaded and no AI model is involved.** The only other program it runs is
50
51
  [Tesseract](https://github.com/tesseract-ocr/tesseract), also on your machine and optional, to read
@@ -111,6 +112,41 @@ A scale read from labels always comes with a statement of how far to trust it an
111
112
  left uncalibrated (values from 0 to 1) and you are told so: the aim is never to pass off a wrong
112
113
  scale as a good one ([how well that is met](#how-accurate-is-it)).
113
114
 
115
+ ## Other kinds of figure
116
+
117
+ Points, bars, stacked rows, heat maps and polar plots each have a reader of their own. They find
118
+ the plot areas and read the axes exactly as `digitise` does, take the same settings for them
119
+ (`y_min`, `y_ref`, `plot_area`, `exclude` …), and give a table with a row for each thing found:
120
+
121
+ | Figure | Python | Command line | One row for each |
122
+ |---|---|---|---|
123
+ | Scatter or dot plot, line with markers | `digitise_points` | `trace-digitiser points` | point, with its error bars |
124
+ | Bar chart | `digitise_bars` | `trace-digitiser bars` | bar: where it ends, and its error bar |
125
+ | Actogram, double-plotted record, any stack of rows | `digitise_rows` | `trace-digitiser rows` | bin (or column) of each row |
126
+ | Heat map with a colour bar | `digitise_heatmap` | `trace-digitiser heatmap` | cell, from its colour on the bar |
127
+ | Polar plot | `digitise_polar` | `trace-digitiser polar` | point: its angle and distance |
128
+
129
+ ```python
130
+ from trace_digitiser import digitise_points, digitise_bars, digitise_rows, digitise_heatmap
131
+
132
+ digitise_points("forest.png", save_overlay=True) # markers, joined by a line or not; error bars too
133
+ digitise_bars("bars.png", y_min=-25, y_max=0) # the legend's patch is left out
134
+ digitise_rows("birthogram.png", row_values=range(1955, 2014), row_spacing_value=1.1,
135
+ bins=24, dots="black") # each row's fill, month by month, and its dot
136
+ digitise_heatmap("ccf.png", rows=21) # the colour bar's labels are read too
137
+ ```
138
+
139
+ Each returns a `FigureResult`: `result.data` (the table), `result.warnings`, `result.overlay()`
140
+ and the files written (`<image>_points.csv`, `<image>_points_provenance.json` …).
141
+
142
+ For a program with a graphical interface — the
143
+ [digitiser workbench](https://github.com/comparativechrono/digitiser-workbench), which runs all of
144
+ this in a web browser, where you can mark plot areas, ticks and colours with the mouse —
145
+ `describe_figure` reports what the automatic steps find (plot areas, the tick marks on each axis,
146
+ each axis's tick labels and what was read from them, the colours drawn, a colour bar, polar axes)
147
+ so that they can be shown and corrected, and `pixels_to_values` turns points picked by hand into
148
+ the figure's units. `trace-digitiser describe figure.png` prints the same as JSON.
149
+
114
150
  ## Check the result
115
151
 
116
152
  1. **Look at the overlay** (`--overlay`). It shows the plot area that was found, the centre and
@@ -180,6 +216,7 @@ the top-left corner of the image, starting at 0; any image viewer shows them.
180
216
  | The wrong number of panels | `--layout`, `--rows`, `--cols`, `--panels` | `layout_mode`, `expected_rows`, … |
181
217
  | The axis continues beyond its last tick, or has no line at the top or right | `--y-ref ROW:VALUE` twice or more (also `--x-ref`) | `y_ref=[(row, value), …]` |
182
218
  | A logarithmic axis, with limits given | `--y-scale log` | `y_scale="log"` |
219
+ | A line drawn against a second y axis, on the right | `--y-axis right` (with `--trace-colour`) | `y_axis="right"` |
183
220
  | A trace is missed, or two are merged | `--trace-colour RRGGBB` (repeat per trace) | `trace_colours=["#578dd2", …]` |
184
221
  | A legend or annotation is taken for a trace | `--exclude LEFT,TOP,RIGHT,BOTTOM` | `exclude=(l, t, r, b)` |
185
222
  | A black trace is invented or ignored | `--achromatic never` / `always` | `achromatic="never"` |
@@ -230,6 +267,18 @@ Labels under 8 pixels high are never given high confidence, however well they ag
230
267
  that small is misread in consistent ways — the decimal point dropped from every label, say. If
231
268
  your figure has such labels, enlarging the image does not help; typing in two numbers does.
232
269
 
270
+ **Published figures.** The tests above draw their own figures.
271
+ [`validation/published-figures`](https://github.com/comparativechrono/trace-digitiser/tree/main/validation/published-figures)
272
+ holds seven figures from four papers whose authors published the numbers behind them — markers with error bars, a
273
+ line over shaded bands, ten bar charts, dot plots, and a scanned page from 1920 — and reads them with nothing given
274
+ and with the help a user can give. With nothing given, 2.2.0 reads all 35 bars of the bar charts to within 0.3 % of
275
+ the axis, and the 29 points of a scatter plot of four series to within 0.5 % (2.1.0 read none of either); with two
276
+ ticks clicked on each axis and the dots' colour, the census dots of the 1920 figure to within 0.9 %. On the figures
277
+ of a paper held out until the changes were made, with the plot area drawn, it reads 24 bars to within 0.4 % once
278
+ two ticks are clicked, and 33 of 37 points once the ticks of a slanted axis and the colours are. Stacked plots with no gap between them, dot
279
+ plots whose dots overlap, and tick labels that are slanted or hand-lettered still need help or remain hard; that
280
+ folder's README has the details.
281
+
233
282
  ### Test it on data you know
234
283
 
235
284
  The best test of a digitisation is one on data like yours. `round_trip` draws a known series as a
@@ -253,14 +302,19 @@ does this for a week of minute-by-minute activity drawn at the size of a small j
253
302
  that is askew is straightened automatically if the tilt is under five degrees; beyond that,
254
303
  or for a photograph taken at an angle (where the axes are no longer parallel), correct the
255
304
  image first.
256
- * One trace per colour per panel. Two traces of the same colour, dashed or dotted lines, and
257
- traces that are only markers are not separated or joined up.
305
+ * One trace per colour per panel. Two traces of the same colour and dashed or dotted lines are not
306
+ separated or joined up. Markers are read by `digitise_points`.
258
307
  * Where traces cross or overlap, the one drawn underneath is hidden and its values there are
259
- interpolated (`interpolated` is `True`).
260
- * Axis scales are read only from numeric labels on linear or logarithmic axes; dates, broken axes
261
- and a second y axis are not understood. Limits typed in are assumed to be at the axis lines
262
- unless `y_ref` / `x_ref` say otherwise.
263
- * Error bars, shaded bands, bars and scatter plots are not digitised.
308
+ interpolated (`interpolated` is `True`). Markers that overlap heavily (the inner turns of a
309
+ spiral on a polar plot, say) are separated by their size, and some may be missed or doubled:
310
+ check the overlay.
311
+ * Axis scales are read only from numeric labels on linear or logarithmic axes (on the left, below,
312
+ or for a colour bar or a second y axis, on the right); dates, month names and broken axes are
313
+ not understood: give `x_ref` / `y_ref` for those. Limits typed in are assumed to be at the axis
314
+ lines unless `y_ref` / `x_ref` say otherwise.
315
+ * Shaded bands are not digitised. A fill drawn with an outline of its own colour (matplotlib's
316
+ `fill_between`) is measured to the middle of the outline; bars are measured to their edges, to
317
+ the nearest pixel where the bars' edges are not antialiased.
264
318
 
265
319
  ## Development
266
320
 
@@ -270,6 +324,7 @@ cd trace-digitiser
270
324
  pip install -e ".[dev]"
271
325
  pytest # the tests that need Tesseract are skipped without it
272
326
  ruff check src tests
327
+ python validation/published-figures/run.py # figures from papers against their authors' data (minutes)
273
328
  ```
274
329
 
275
330
  `src/trace_digitiser/` in the order the work is done:
@@ -284,6 +339,11 @@ ruff check src tests
284
339
  | `ocr.py`, `summarise.py` | Interval labels along the x axis, and means per interval. |
285
340
  | `diagnostics.py`, `provenance.py`, `io.py` | Overlay, record of the run, files. |
286
341
  | `validation.py`, `synthetic.py` | Figures drawn from known data, and the comparisons. |
342
+ | `_figure.py` | What every reader does first: the image, straightening, plot areas and axes. |
343
+ | `markers.py`, `points.py` | Markers and their error bars. |
344
+ | `bars.py`, `rows.py`, `heatmap.py`, `polar.py` | Bar charts, stacks of rows, heat maps and polar plots. |
345
+ | `explore.py` | `describe_figure` and `pixels_to_values`, for graphical tools. |
346
+ | `_results.py` | What the readers of points, bars, rows, heat maps and polar plots give back. |
287
347
  | `cli.py` | The command line. |
288
348
 
289
349
  Changes are listed in [CHANGELOG.md](https://github.com/comparativechrono/trace-digitiser/blob/main/CHANGELOG.md);
@@ -1,10 +1,11 @@
1
1
  # trace-digitiser
2
2
 
3
- Get the numbers back out of a line plot when the figure is all that survives — on your own computer.
3
+ Get the numbers back out of a figure when the figure is all that survives — on your own computer.
4
4
 
5
5
  `trace-digitiser` takes an image of a figure (a scan of an old paper, a figure saved from a PDF),
6
- finds the plot area, follows each trace, and writes the values to a CSV file. It is ordinary image
7
- processing, written to be read and checked.
6
+ finds the plot area, follows each trace, and writes the values to a CSV file. It reads line plots,
7
+ and also [points, bars, actograms, heat maps and polar plots](#other-kinds-of-figure). It is
8
+ ordinary image processing, written to be read and checked.
8
9
 
9
10
  **Nothing is uploaded and no AI model is involved.** The only other program it runs is
10
11
  [Tesseract](https://github.com/tesseract-ocr/tesseract), also on your machine and optional, to read
@@ -71,6 +72,41 @@ A scale read from labels always comes with a statement of how far to trust it an
71
72
  left uncalibrated (values from 0 to 1) and you are told so: the aim is never to pass off a wrong
72
73
  scale as a good one ([how well that is met](#how-accurate-is-it)).
73
74
 
75
+ ## Other kinds of figure
76
+
77
+ Points, bars, stacked rows, heat maps and polar plots each have a reader of their own. They find
78
+ the plot areas and read the axes exactly as `digitise` does, take the same settings for them
79
+ (`y_min`, `y_ref`, `plot_area`, `exclude` …), and give a table with a row for each thing found:
80
+
81
+ | Figure | Python | Command line | One row for each |
82
+ |---|---|---|---|
83
+ | Scatter or dot plot, line with markers | `digitise_points` | `trace-digitiser points` | point, with its error bars |
84
+ | Bar chart | `digitise_bars` | `trace-digitiser bars` | bar: where it ends, and its error bar |
85
+ | Actogram, double-plotted record, any stack of rows | `digitise_rows` | `trace-digitiser rows` | bin (or column) of each row |
86
+ | Heat map with a colour bar | `digitise_heatmap` | `trace-digitiser heatmap` | cell, from its colour on the bar |
87
+ | Polar plot | `digitise_polar` | `trace-digitiser polar` | point: its angle and distance |
88
+
89
+ ```python
90
+ from trace_digitiser import digitise_points, digitise_bars, digitise_rows, digitise_heatmap
91
+
92
+ digitise_points("forest.png", save_overlay=True) # markers, joined by a line or not; error bars too
93
+ digitise_bars("bars.png", y_min=-25, y_max=0) # the legend's patch is left out
94
+ digitise_rows("birthogram.png", row_values=range(1955, 2014), row_spacing_value=1.1,
95
+ bins=24, dots="black") # each row's fill, month by month, and its dot
96
+ digitise_heatmap("ccf.png", rows=21) # the colour bar's labels are read too
97
+ ```
98
+
99
+ Each returns a `FigureResult`: `result.data` (the table), `result.warnings`, `result.overlay()`
100
+ and the files written (`<image>_points.csv`, `<image>_points_provenance.json` …).
101
+
102
+ For a program with a graphical interface — the
103
+ [digitiser workbench](https://github.com/comparativechrono/digitiser-workbench), which runs all of
104
+ this in a web browser, where you can mark plot areas, ticks and colours with the mouse —
105
+ `describe_figure` reports what the automatic steps find (plot areas, the tick marks on each axis,
106
+ each axis's tick labels and what was read from them, the colours drawn, a colour bar, polar axes)
107
+ so that they can be shown and corrected, and `pixels_to_values` turns points picked by hand into
108
+ the figure's units. `trace-digitiser describe figure.png` prints the same as JSON.
109
+
74
110
  ## Check the result
75
111
 
76
112
  1. **Look at the overlay** (`--overlay`). It shows the plot area that was found, the centre and
@@ -140,6 +176,7 @@ the top-left corner of the image, starting at 0; any image viewer shows them.
140
176
  | The wrong number of panels | `--layout`, `--rows`, `--cols`, `--panels` | `layout_mode`, `expected_rows`, … |
141
177
  | The axis continues beyond its last tick, or has no line at the top or right | `--y-ref ROW:VALUE` twice or more (also `--x-ref`) | `y_ref=[(row, value), …]` |
142
178
  | A logarithmic axis, with limits given | `--y-scale log` | `y_scale="log"` |
179
+ | A line drawn against a second y axis, on the right | `--y-axis right` (with `--trace-colour`) | `y_axis="right"` |
143
180
  | A trace is missed, or two are merged | `--trace-colour RRGGBB` (repeat per trace) | `trace_colours=["#578dd2", …]` |
144
181
  | A legend or annotation is taken for a trace | `--exclude LEFT,TOP,RIGHT,BOTTOM` | `exclude=(l, t, r, b)` |
145
182
  | A black trace is invented or ignored | `--achromatic never` / `always` | `achromatic="never"` |
@@ -190,6 +227,18 @@ Labels under 8 pixels high are never given high confidence, however well they ag
190
227
  that small is misread in consistent ways — the decimal point dropped from every label, say. If
191
228
  your figure has such labels, enlarging the image does not help; typing in two numbers does.
192
229
 
230
+ **Published figures.** The tests above draw their own figures.
231
+ [`validation/published-figures`](https://github.com/comparativechrono/trace-digitiser/tree/main/validation/published-figures)
232
+ holds seven figures from four papers whose authors published the numbers behind them — markers with error bars, a
233
+ line over shaded bands, ten bar charts, dot plots, and a scanned page from 1920 — and reads them with nothing given
234
+ and with the help a user can give. With nothing given, 2.2.0 reads all 35 bars of the bar charts to within 0.3 % of
235
+ the axis, and the 29 points of a scatter plot of four series to within 0.5 % (2.1.0 read none of either); with two
236
+ ticks clicked on each axis and the dots' colour, the census dots of the 1920 figure to within 0.9 %. On the figures
237
+ of a paper held out until the changes were made, with the plot area drawn, it reads 24 bars to within 0.4 % once
238
+ two ticks are clicked, and 33 of 37 points once the ticks of a slanted axis and the colours are. Stacked plots with no gap between them, dot
239
+ plots whose dots overlap, and tick labels that are slanted or hand-lettered still need help or remain hard; that
240
+ folder's README has the details.
241
+
193
242
  ### Test it on data you know
194
243
 
195
244
  The best test of a digitisation is one on data like yours. `round_trip` draws a known series as a
@@ -213,14 +262,19 @@ does this for a week of minute-by-minute activity drawn at the size of a small j
213
262
  that is askew is straightened automatically if the tilt is under five degrees; beyond that,
214
263
  or for a photograph taken at an angle (where the axes are no longer parallel), correct the
215
264
  image first.
216
- * One trace per colour per panel. Two traces of the same colour, dashed or dotted lines, and
217
- traces that are only markers are not separated or joined up.
265
+ * One trace per colour per panel. Two traces of the same colour and dashed or dotted lines are not
266
+ separated or joined up. Markers are read by `digitise_points`.
218
267
  * Where traces cross or overlap, the one drawn underneath is hidden and its values there are
219
- interpolated (`interpolated` is `True`).
220
- * Axis scales are read only from numeric labels on linear or logarithmic axes; dates, broken axes
221
- and a second y axis are not understood. Limits typed in are assumed to be at the axis lines
222
- unless `y_ref` / `x_ref` say otherwise.
223
- * Error bars, shaded bands, bars and scatter plots are not digitised.
268
+ interpolated (`interpolated` is `True`). Markers that overlap heavily (the inner turns of a
269
+ spiral on a polar plot, say) are separated by their size, and some may be missed or doubled:
270
+ check the overlay.
271
+ * Axis scales are read only from numeric labels on linear or logarithmic axes (on the left, below,
272
+ or for a colour bar or a second y axis, on the right); dates, month names and broken axes are
273
+ not understood: give `x_ref` / `y_ref` for those. Limits typed in are assumed to be at the axis
274
+ lines unless `y_ref` / `x_ref` say otherwise.
275
+ * Shaded bands are not digitised. A fill drawn with an outline of its own colour (matplotlib's
276
+ `fill_between`) is measured to the middle of the outline; bars are measured to their edges, to
277
+ the nearest pixel where the bars' edges are not antialiased.
224
278
 
225
279
  ## Development
226
280
 
@@ -230,6 +284,7 @@ cd trace-digitiser
230
284
  pip install -e ".[dev]"
231
285
  pytest # the tests that need Tesseract are skipped without it
232
286
  ruff check src tests
287
+ python validation/published-figures/run.py # figures from papers against their authors' data (minutes)
233
288
  ```
234
289
 
235
290
  `src/trace_digitiser/` in the order the work is done:
@@ -244,6 +299,11 @@ ruff check src tests
244
299
  | `ocr.py`, `summarise.py` | Interval labels along the x axis, and means per interval. |
245
300
  | `diagnostics.py`, `provenance.py`, `io.py` | Overlay, record of the run, files. |
246
301
  | `validation.py`, `synthetic.py` | Figures drawn from known data, and the comparisons. |
302
+ | `_figure.py` | What every reader does first: the image, straightening, plot areas and axes. |
303
+ | `markers.py`, `points.py` | Markers and their error bars. |
304
+ | `bars.py`, `rows.py`, `heatmap.py`, `polar.py` | Bar charts, stacks of rows, heat maps and polar plots. |
305
+ | `explore.py` | `describe_figure` and `pixels_to_values`, for graphical tools. |
306
+ | `_results.py` | What the readers of points, bars, rows, heat maps and polar plots give back. |
247
307
  | `cli.py` | The command line. |
248
308
 
249
309
  Changes are listed in [CHANGELOG.md](https://github.com/comparativechrono/trace-digitiser/blob/main/CHANGELOG.md);
@@ -23,14 +23,14 @@ This has to be done once by an owner of the `trace-digitiser` project on PyPI.
23
23
 
24
24
  3. Click **Add**.
25
25
 
26
- That is all. The link works while the repository is private.
26
+ That is all.
27
27
 
28
28
  ### Optional hardening
29
29
 
30
30
  PyPI recommends tying the publisher to a GitHub *environment*, which lets you
31
- require a manual approval before each upload. GitHub only offers environments
32
- on private repositories for paid plans, so the workflow does not use one. Once
33
- the repository is public (or on a paid plan):
31
+ require a manual approval before each upload. The workflow does not use one
32
+ yet (when the repository was private, GitHub offered environments only on paid
33
+ plans). Now that the repository is public, one can be added:
34
34
 
35
35
  1. Create an environment called `pypi` under *Settings -> Environments*
36
36
  (optionally with required reviewers).
@@ -39,12 +39,27 @@ the repository is public (or on a paid plan):
39
39
 
40
40
  ## Making a release
41
41
 
42
- 1. Set `version` in `pyproject.toml` (PyPI never accepts the same version
43
- twice; the last published version is 1.2.0).
42
+ 1. Set `version` in `pyproject.toml`. PyPI never accepts the same version
43
+ twice; the versions already published are listed at
44
+ <https://pypi.org/project/trace-digitiser/#history>.
44
45
  2. In `CHANGELOG.md`, give the section for the version its date in place of
45
- "not yet released".
46
- 3. Merge to `main` and wait for the *Tests* workflow to pass.
47
- 4. On GitHub: *Releases -> Draft a new release*, create the tag `vX.Y.Z`
46
+ "unreleased", and end its link at the new tag (`...vX.Y.Z` in place of
47
+ `...HEAD`).
48
+ 3. Run the published-figure test set
49
+ ([`validation/published-figures`](validation/published-figures/README.md)),
50
+ which reads figures from papers whose data are published, and compare the
51
+ results with the last release's:
52
+
53
+ ```bash
54
+ python validation/published-figures/run.py \
55
+ --json validation/published-figures/results-X.Y.Z.json \
56
+ --compare validation/published-figures/results-<last release>.json
57
+ ```
58
+
59
+ Look into any case marked WORSE before going on. Commit the new results,
60
+ and update the table in that folder's README (`--markdown` prints it).
61
+ 4. Merge to `main` and wait for the *Tests* workflow to pass.
62
+ 5. On GitHub: *Releases -> Draft a new release*, create the tag `vX.Y.Z`
48
63
  (matching the version exactly, with a leading `v`) on `main`, and publish.
49
64
 
50
65
  Publishing the release starts the workflow. It checks that the tag matches the
@@ -53,8 +68,7 @@ uploads to PyPI. If the tag and version disagree, or a test fails, the run
53
68
  stops before anything is uploaded.
54
69
 
55
70
  The README is shown on PyPI as it is in the release. Its links to files in
56
- this repository are absolute, so they work there only once the repository is
57
- public.
71
+ this repository are absolute, so they work there too.
58
72
 
59
73
  ## Checking a build without releasing
60
74
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trace-digitiser"
7
- version = "2.0.0"
8
- description = "Recover the data from line plots in figure images, on your own computer: no uploads, no AI model."
7
+ version = "2.2.0"
8
+ description = "Recover the data from figure images - lines, points, bars, actograms, heat maps - on your own computer: no uploads, no AI model."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
@@ -20,6 +20,10 @@ keywords = [
20
20
  "time series",
21
21
  "circadian",
22
22
  "actigraphy",
23
+ "actogram",
24
+ "heat map",
25
+ "scatter plot",
26
+ "bar chart",
23
27
  ]
24
28
  classifiers = [
25
29
  "Development Status :: 4 - Beta",