svgdigitizer.svgplot

Scientific plots in SVG format.

A SVGPlot wraps a plot in SVG format consisting of a curve, axis labels and (optionally) additional metadata provided as text fields in the SVG.

As an example, consider the graph given by the line segment connecting (0, 0) and (1, 1). In the SVG coordinate system, such a line segment is given as the path connecting the points (0, 100) and (100, 0); note that the SVG coordinate system is negative, i.e., the y-axis grows towards the bottom:

>>> curve = '<path d="M 0 100 L 100 0" />'

We need to attach a label to this path, so SVGPlot understands that this is the actual curve that contains data we want to digitize. We do so by grouping this path with a label, that says “curve: …”. The position of that label has no importance but is required. The identifier itself does also not matter in this example. It is only relevant when there are multiple curves in the same SVG:

>>> curve = f'''
...     <g>
...       { curve }
...       <text x="0" y="0">curve: 0</text>
...     </g>
... '''

Additionally, we need to establish a plot coordinate system. We do so by creating ticks for two reference ticks for both the x-axis and the y-axis. To start, we want to define that the y=100 in the SVG coordinate system corresponds to y=0 in the plot coordinate system:

>>> y1 = '<text x="-100" y="100">y1: 0</text>'

The location of this reference label does not matter much, we just put it somewhere where it looks nice. Now we need to pinpoint the place on the y-axis that corresponds to y=0. We do so by drawing a path from close to the base point of the reference label to that point on the y-axis and group it with the label:

>>> y1 = f'''
...     <g>
...       <path d="M -100 100 L 0 100" />
...       { y1 }
...     </g>
... '''

We repeat the same process for the other reference labels, i.e., y2, x1, x2 and obtain our input SVG that SVGPlot can make sense of. Note that we added some units, so the x-axis is the time in seconds, and the y-axis is a voltage in volts.

>>> svg = f'''
...     <svg>
...       { curve }
...       <g>
...         <path d="M 0 200 L 0 100" />
...         <text x="0" y="200">x1: 0</text>
...       </g>
...       <g>
...         <path d="M 100 200 L 100 100" />
...         <text x="100" y="200">x2: 1s</text>
...       </g>
...       { y1 }
...       <g>
...         <path d="M -100 0 L 0 0" />
...         <text x="-100" y="0">y2: 1V</text>
...       </g>
...     </svg>
... '''

We wrap this string into an svg.SVG object and create an actual SVGPlot from it:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(svg))
>>> plot = SVGPlot(svg)

Now we can query the plot for things such as the units used on the axes:

>>> plot.axis_labels
{'x': 's', 'y': 'V'}

We can get a pandas data frame with actual plot data in the plot coordinate system:

>>> plot.df
     x    y
0  0.0  0.0
1  1.0  1.0

This data frame is built from the end points of the paths that make up the curve. We can also interpolate at equidistant points on the x-axis by specifying a sampling_interval:

>>> plot = SVGPlot(svg, sampling_interval=.1)
>>> plot.df
      x    y
0   0.0  0.0
1   0.1  0.1
2   0.2  0.2
3   0.3  0.3
4   0.4  0.4
5   0.5  0.5
6   0.6  0.6
7   0.7  0.7
8   0.8  0.8
9   0.9  0.9
10  1.0  1.0
class svgdigitizer.svgplot.AxisOrientation(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

The orientation of a plot axis.

class svgdigitizer.svgplot.SVGPlot(svg, sampling_interval=None, curve=None, algorithm='axis-aligned')

A plot as a Scalable Vector Graphics (SVG) which can be converted to (x, y) coordinate pairs.

Typically, the SVG input has been created by tracing a measurement plot from a publication with a <path> in an SVG editor such as Inkscape. Such a path can then be analyzed by this class to produce the coordinates corresponding to the original measured values.

INPUT:

  • algorithm – mapping from the SVG coordinate system to the plot coordinate system. The default, “axis-aligned” assumes that the plot axes are perfectly aligned with the SVG coordinate system. Alternatively, “mark-aligned” assumes the end points of the axis markers are aligned, i.e., x1 and x2 have the exact same y coordinate in the plot coordinate system to also handle coordinate systems that are rotated or skewed in the SVG.

EXAMPLES:

An instance of this class can be created from a specially prepared SVG file. There must at least be a <path> with a corresponding label. Here, a segment goes from (0, 100) to (100, 0) in the (negative) SVG coordinate system which corresponds to a segment from (0, 0) to (1, 1) in the plot coordinate system:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.df
     x    y
0  0.0  0.0
1  1.0  1.0
property axis_labels

Return the label for each axis as dict with variable as key and unit as value.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1cm</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">j1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">j2: 1 A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.axis_labels
{'E': 'cm', 'j': 'A'}

TESTS:

Labels on the axes must match:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1m</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> from unittest import TestCase
>>> with TestCase.assertLogs(_) as logs:
...    plot.axis_labels
...    print(logs.output)
{'x': 'm', 'y': None}
['WARNING:svgplot:Labels on x axis do not match. Will ignore label cm and use m.']

Labels on the scalebar must match the labels on the axes:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1m</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0mA</text>
...   </g>
...   <g>
...     <path d="M -300 300 L -200 300" />
...     <path d="M -300 300 L -200 200" />
...     <text x="-300" y="300">y_scale_bar: 1A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> with TestCase.assertLogs(_) as logs:
...    plot.axis_labels
...    print(logs.output)
{'x': 'm', 'y': 'A'}
['WARNING:svgplot:Labels on y axis do not match. Will ignore label mA and use A.']
property axis_orientations

Return the Orientation for each axis.

ALGORITHM:

We suppose that one axis was meant to be the horizontal axis and one axis was meant to be the vertical axis. Under this assumption we compute the transformation that makes the axes perfectly horizontal and vertical. We determine how much rotation is needed in this transformation. Now we exchange the roles of the axes and again the amount of rotation needed. We then label the axes such that the amount of rotation is minimized.

Naturally, this does not work very well when the axes are at almost 45° and it is not clear which axis was meant to be horizontal and vertical.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1cm</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">j1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">j2: 1 A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.axis_orientations
{<AxisOrientation.HORIZONTAL: 'horizontal'>: 'E', <AxisOrientation.VERTICAL: 'vertical'>: 'j'}
property axis_variables

Return the label for each axis.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1cm</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">j1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">j2: 1 A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.axis_variables
['E', 'j']

>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1cm</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">intensity1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">intensity2: 1 A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.axis_variables
['E', 'intensity']
property curve

Return the path that is tracing the plot in the SVG.

This essentially returns the <path> tag that is not used for other purposes such as pointing to axis labels. However, the path is written in the plot coordinate system.

EXAMPLES:

A plot going from (0, 0) to (1, 1):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.curve
Path(Line(start=0j, end=(1+1j)))

TESTS:

Test that filtering by curve identifier works:

>>> plot = SVGPlot(svg, curve="main curve")
>>> plot.curve
Traceback (most recent call last):
...
svgdigitizer.exceptions.SVGAnnotationError: No paths labeled 'curve: main curve' found.
property df

Return the plot data as a dataframe of pairs (x, y).

The returned data lives in the plot coordinate system.

EXAMPLES:

A diagonal from (0, 100) to (100, 0) in the SVG coordinate system, i.e., the function y=x:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.df
     x    y
0  0.0  0.0
1  1.0  1.0

The same plot but now sampled at 0.2 increments on the x-axis:

>>> plot = SVGPlot(svg, sampling_interval=.2)
>>> plot.df
     x    y
0  0.0  0.0
1  0.2  0.2
2  0.4  0.4
3  0.6  0.6
4  0.8  0.8
5  1.0  1.0

Again diagonal from (0, 100) to (100, 50) in the SVG coordinate system, i.e., visually the function y=x/2. However, the coordinate system is skewed, the x-axis is parallel to the plot and so this is actually the function y=0:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 50" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 50" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg, algorithm='mark-aligned')
>>> plot.df
     x    y
0  0.0  0.0
1  1.0  0.0

The same plot but now sampled at 0.2 increments on the x-axis:

>>> plot = SVGPlot(svg, sampling_interval=.2, algorithm='mark-aligned')
>>> plot.df
     x    y
0  0.0  0.0
1  0.2  0.0
2  0.4  0.0
3  0.6  0.0
4  0.8  0.0
5  1.0  0.0
property figure_schema

A frictionless Schema object, including a Fields object describing the dimensions, units and orientation of the original plot axes.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from svgdigitizer.svgplot import SVGPlot
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">t1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">t2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.figure_schema
{'fields': [{'name': 't', 'type': 'number', 'unit': None, 'orientation': 'x'},
            {'name': 'y', 'type': 'number', 'unit': None, 'orientation': 'y'}]}
from_svg(x, y)

Map the point (x, y) from the SVG coordinate system to the plot coordinate system.

EXAMPLES:

A simple plot. The plot uses a Cartesian (positive) coordinate system which in the SVG becomes a negative coordinate system, i.e., in the plot y grows towards the bottom. Here, the SVG coordinate (0, 100) is mapped to (0, 0) and (100, 0) is mapped to (1, 1):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.from_svg(0, 100)
(0.0, 0.0)
>>> plot.from_svg(100, 0)
(1.0, 1.0)

A typical plot. Like the above but the origin is shifted and the two axes are not scaled equally. Here (1024, 512) is mapped to (0, 0) and (1124, 256) is mapped to (1, 1):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 1024 612 L 1024 512" />
...     <text x="1024" y="612">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 1124 612 L 1124 512" />
...     <text x="1124" y="612">x2: 1</text>
...   </g>
...   <g>
...     <path d="M 924 512 L 1024 512" />
...     <text x="924" y="512">y1: 0</text>
...   </g>
...   <g>
...     <path d="M 924 256 L 1024 256" />
...     <text x="924" y="256">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> from numpy import allclose
>>> allclose(plot.from_svg(1024, 512), (0, 0))
True
>>> allclose(plot.from_svg(1124, 256), (1, 1))
True

A skewed plot. In this plot the axes are not orthogonal. In real plots the axes might be non-orthogonal but not as much as in this example. Here, one axis goes horizontally from (0, 100) to (100, 100) and the other axis goes at an angle from (0, 100) to (100, 0):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M 0 100 L 0 100" />
...     <text x="0" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M 0 0 L 100 0" />
...     <text x="0" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg, algorithm='mark-aligned')
>>> plot.from_svg(0, 100)
(0.0, 0.0)
>>> plot.from_svg(100, 100)
(1.0, 0.0)
>>> plot.from_svg(100, 0)
(0.0, 1.0)
>>> plot.from_svg(0, 0)
(-1.0, 1.0)
property labeled_paths

All paths with their corresponding label.

We only consider paths which are grouped with a label, i.e., a <text> element. These text elements then tell us about the meaning of that path, e.g., the path that is labeled as curve is the actual graph we are going to extract the (x, y) coordinate pairs from.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.labeled_paths
{'ref_point': [], 'scale_bar': [], 'curve': [[Path "curve: 0"]]}

TESTS:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">kurve: 0</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> from unittest import TestCase
>>> with TestCase.assertLogs(_) as warnings:
...     plot.labeled_paths
...     print(warnings.output)
{'ref_point': [], 'scale_bar': [], 'curve': []}
['WARNING:svgplot:Ignoring <path> with unsupported label kurve: 0.']
property marked_points

Return the points that have been marked on the axes of the plot.

For each point, a tuple is returned relating the point’s coordinates in the SVG coordinate system to the point’s coordinates in the plot coordinate system, or None if that point’s coordinate is not known.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.marked_points == {'x2': ((100.0, 100.0), 1.0, None), 'x1': ((0.0, 100.0), 0.0, None), 'y2': ((0.0, 0.0), 1.0, None), 'y1': ((0.0, 100.0), 0.0, None)}
True

TESTS:

Test that scalebars can be parsed:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -300 300 L -200 300" />
...     <path d="M -300 300 L -200 200" />
...     <text x="-300" y="300">y_scale_bar: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.marked_points == {'x1': ((0.0, 100.0), 0.0, None), 'x2': ((100.0, 100.0), 1.0, None), 'y1': ((0.0, 100.0), 0.0, None), 'y2': ((0.0, 0.0), 1.0, None)}
True
plot()

Visualize the data in this plot.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 100 L 100 0" />
...     <text x="0" y="0">curve: 0</text>
...   </g>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1 s</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1 cm</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.plot()
classmethod sample_path(path, sampling_interval, endpoints='include')

Return points on path, sampled at equidistant increments on the x-axis.

INPUT:

  • path – the SVG path to sample along the SVG x-axis.

  • sampling_interval – the distance between two samples on the SVG x-axis.

  • endpoints – whether to include the endpoints of each path segment “include” or not to include them “exclude”; see below for details.

ALGORITHM:

Let us assume that path is made up of Bezier curve segments (the other cases work essentially the same but are easier.) We project the curve down to the x-axis. This projection is stil a Bezier curve, i.e., of the form

B(t) = (1-t)^3 P_0 + 3t(1-t)^2 P_1 + 3t^2(1-t) P_2 + t^3 P_3.

Since all the control points are on the axis, this is just a univariate polynomial of degree 3, in particular we can easily differentiate it, take its absolute value and integrate again. The result is a piecewise function x(t) that is a polynomial of degree three and it encodes the total distance on the x-axis at time t. So given some X we can easily solve for

x(T) = X

Incrementing X by the step size, this gives us all the times T that we wanted to sample for.

Sampling at equidistant increments might actually drop features of the curve. In the most extreme case, a vertical line segment, such sampling returns one (implementation dependent) point on that line segment. When endpoints is set to “include”, we always include the endpoints of each path segment even if they are not spaced by the sampling interval. If set to “exclude” we strictly sample at the sampling interval.

EXAMPLES:

We can sample a pair of line segments:

>>> from svgpathtools.path import Path
>>> path = Path("M 0 0 L 1 1 L 2 0")
>>> SVGPlot.sample_path(path, .5)
[(0.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (2.0, 0.0)]

We can sample a pair of Bezier curves, going from (0, 0) to (2, 0) with a sharp peak at (1, 1):

>>> from svgpathtools.path import Path
>>> path = Path("M0 0 C 1 0, 1 0, 1 1 C 1 0, 1 0, 2 0")
>>> SVGPlot.sample_path(path, 1)
[(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)]
>>> len(SVGPlot.sample_path(path, .5))
5
>>> len(SVGPlot.sample_path(path, .1))
21
>>> len(SVGPlot.sample_path(path, .01))
201

We can sample vertical line segments:

>>> from svgpathtools.path import Path
>>> path = Path("M 0 0 L 0 1 M 1 1 L 1 0")
>>> SVGPlot.sample_path(path, .0001)
[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]
>>> SVGPlot.sample_path(path, .0001, endpoints='exclude')  # the implementation chooses the initial point of the first segment
[(0.0, 1.0)]

TESTS:

A case where numpy’s root finding returns the extrema out of order:

>>> from svgpathtools.path import Path
>>> path = Path("M-267 26 C -261 25, -266 24, -264 23")
>>> len(SVGPlot.sample_path(path, .001))
4159
property scaling_factors

Return the scaling factors for each axis.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <text x="0" y="0">y_scaling_factor: 50.6</text>
...   <text x="0" y="0">xsf: 50.6</text>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.scaling_factors
{'x': 50.6, 'y': 50.6}

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <text x="0" y="0">j_scaling_factor: 24.5</text>
...   <text x="0" y="0">Esf: 18.3</text>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">j1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">j2: 1</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.scaling_factors
{'E': 18.3, 'j': 24.5}
property transformation

Return the affine map from the SVG coordinate system to the plot coordinate system as a matrix, see https://en.wikipedia.org/wiki/Affine_group#Matrix_representation

EXAMPLES:

A simple plot. The plot uses a Cartesian (positive) coordinate system which in the SVG becomes a negative coordinate system, i.e., in the plot y grows towards the bottom. Here, the SVG coordinate (0, 100) is mapped to (0, 0) and (100, 0) is mapped to (1, 1):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> SVGPlot(svg).transformation
array([[ 0.01,  0.  ,  0.  ],
       [ 0.  , -0.01,  1.  ],
       [ 0.  ,  0.  ,  1.  ]])

A typical plot. Like the above but the origin is shifted and the two axes are not scaled equally. Here (1000, 500) is mapped to (0, 0) and (1100, 300) is mapped to (1, 1):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 1000 600 L 1000 500" />
...     <text x="1000" y="600">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 1100 600 L 1100 500" />
...     <text x="1100" y="600">x2: 1</text>
...   </g>
...   <g>
...     <path d="M 900 500 L 1000 500" />
...     <text x="900" y="500">y1: 0</text>
...   </g>
...   <g>
...     <path d="M 900 300 L 1000 300" />
...     <text x="900" y="300">y2: 1</text>
...   </g>
... </svg>'''))
>>> A = SVGPlot(svg).transformation
>>> from numpy import allclose
>>> allclose(A, [
...   [ 0.01,  0.000, -10.00],
...   [ 0.00, -0.005,   2.50],
...   [ 0.00,  0.000,   1.00],
... ])
True

A skewed plot. In this plot the axes are not orthogonal. In real plots the axes might be non-orthogonal but not as much as in this example. Here, one axis goes horizontally from (0, 100) to (100, 100) and the other axis goes at an angle from (0, 100) to (100, 0):

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M 0 100 L 0 100" />
...     <text x="0" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M 0 0 L 100 0" />
...     <text x="0" y="0">y2: 1</text>
...   </g>
... </svg>'''))
>>> SVGPlot(svg, algorithm='mark-aligned').transformation
array([[ 0.01,  0.01, -1.  ],
       [ 0.  , -0.01,  1.  ],
       [ 0.  ,  0.  ,  1.  ]])

A skewed plot like the one above but with a scale bar:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">x1: 0</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">x2: 1</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">y1: 0</text>
...   </g>
...   <g>
...     <path d="M -300 300 L -200 300" />
...     <path d="M -300 300 L -100 200" />
...     <text x="-300" y="300">y_scale_bar: 1</text>
...   </g>
... </svg>'''))
>>> SVGPlot(svg, algorithm='mark-aligned').transformation
array([[ 0.01,  0.01, -1.  ],
       [ 0.  , -0.01,  1.  ],
       [ 0.  ,  0.  ,  1.  ]])
property xlabel

Return the label of the x axis.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1cm</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">j1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">j2: 1 A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.xlabel
'E'
property ylabel

Return the label of the y axis.

EXAMPLES:

>>> from svgdigitizer.svg import SVG
>>> from io import StringIO
>>> svg = SVG(StringIO(r'''
... <svg>
...   <g>
...     <path d="M 0 200 L 0 100" />
...     <text x="0" y="200">E1: 0 cm</text>
...   </g>
...   <g>
...     <path d="M 100 200 L 100 100" />
...     <text x="100" y="200">E2: 1cm</text>
...   </g>
...   <g>
...     <path d="M -100 100 L 0 100" />
...     <text x="-100" y="100">j1: 0</text>
...   </g>
...   <g>
...     <path d="M -100 0 L 0 0" />
...     <text x="-100" y="0">j2: 1 A</text>
...   </g>
... </svg>'''))
>>> plot = SVGPlot(svg)
>>> plot.ylabel
'j'