Module generativepy.graph
Expand source code
# Author: Martin McBride
# Created: 2019-06-04
# Copyright (C) 2018, Martin McBride
# License: MIT
import itertools
import cairo
import math
import numpy as np
import copy
from dataclasses import dataclass
from generativepy.geometry import Text, Shape, FillParameters, StrokeParameters, FontParameters, Circle, Polygon, Line
from generativepy.drawing import BUTT, FONT_WEIGHT_BOLD, FONT_SLANT_NORMAL, WINDING, SQUARE, MITER
from generativepy.color import Color
from generativepy import drawing
# Point styles for graphs
POINT_CIRCLE = 0 # Circular points
# Point styles for graphs
SCATTER_NO_LINE = 0 # All points on scatter chart are left unconnected
SCATTER_STALK = 1 # Stalk chart style
SCATTER_CONNECTED = 2 # Points are joined one to the next
@dataclass
class AxesAppearance:
'''
Parameters that control the appearance of the axes (colours, line styles).
'''
background = FillParameters(Color(1))
textcolor = Color(0.2)
fontparams = FontParameters('arial', size=15, weight=FONT_WEIGHT_BOLD)
divlines = StrokeParameters(Color(0.8, 0.8, 1), line_width=2, cap=BUTT)
subdivlines = StrokeParameters(Color(0.9, 0.9, 1), line_width=2, cap=BUTT)
axislines = StrokeParameters(Color(0.2), line_width=2, cap=BUTT)
featurescale = 1
# x and y offset of tick labels. The actual offset is:
# text height (height of 0 character using fontparams) DIVIDED by ticklabeloffset
ticklabeloffset = 1.1
class Axes:
'''
Controls the range and appearance of a set of Cartesian axes
'''
def __init__(self, ctx, position, width, height):
self.ctx = ctx
self.appearance = AxesAppearance()
self.position = position
self.width = width
self.height = height
self.start = (0, 0)
self.extent = (10, 10)
self.divisions = (1, 1)
self.subdivisons = False
self.subdivisionfactor = (1, 1)
self.text_height = 0
self.x_div_formatter = None
self.y_div_formatter = None
def of_start(self, start):
'''
Sets the start value of the axes
Args:
start: (x, y) value of bottom left corner of axes
Returns:
self
'''
self.start = start
return self
def of_extent(self, extent):
'''
Sets the range of the axes
Args:
extent: (x, y) range of axes
Returns:
self
'''
self.extent = extent
return self
def with_feature_scale(self, scale):
'''
Sets the scale of the features. For example a value of 2 will make all the gridlines and label text
on the axes twice as big. This is a quick way of resizing everything in one go.
Args:
scale: scale factor
Returns:
self
'''
self.appearance.featurescale = scale
return self
def with_divisions(self, divisions):
'''
Set divisons spacing
Args:
divisions: (x, y) spacing divisions in each direction
Returns:
self
'''
self.divisions = divisions
return self
def with_division_formatters(self, x_div_formatter=None, y_div_formatter=None):
self.x_div_formatter = x_div_formatter
self.y_div_formatter = y_div_formatter
return self
def with_subdivisions(self, factor):
'''
Draw subdivision lines on graph
Args:
factor: (x, y) Number of subdivisions per division in each direction
Returns:
self
'''
self.subdivisons = True
self.subdivisionfactor = factor
return self
def background(self, pattern):
'''
Sets the entire graph background
Args:
pattern: color or fill pattern
Returns:
self
'''
self.appearance.background = FillParameters(pattern)
return self
def text_color(self, pattern):
'''
Sets the color of the axes text
Args:
pattern: color or pattern
Returns:
self
'''
self.appearance.textcolor = pattern
return self
def text_style(self, font="arial", weight=FONT_WEIGHT_BOLD, slant=FONT_SLANT_NORMAL, size=15):
'''
Set the style of the axis text
Args:
font: Font name
weight: Font weight
slant: Font slant
size: Font size in units. This will be multiplied by the featurescale value.
Returns:
self
'''
self.appearance.fontparams = FontParameters(font, weight, slant, size)
return self
def axis_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None):
'''
Sets the style of the axis lines
Args:
pattern: the fill pattern or color to use for the outline, None for default
line_width: width of stroke line, None for default
dash: dash patter of line, as for Pycairo, None for default
cap: line end style, None for default
join: line join style, None for default
miter_limit: mitre limit, None for default
Returns:
self
'''
self.appearance.axislines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit)
return self
def division_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None):
'''
Sets the style of the division lines
Args:
pattern: the fill pattern or color to use for the outline, None for default
line_width: width of stroke line, None for default
dash: dash patter of line, as for Pycairo, None for default
cap: line end style, None for default
join: line join style, None for default
miter_limit: mitre limit, None for default
Returns:
self
'''
self.appearance.divlines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit)
return self
def subdivision_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None):
'''
Sets the style of the subdivision lines
Args:
pattern: the fill pattern or color to use for the outline, None for default
line_width: width of stroke line, None for default
dash: dash patter of line, as for Pycairo, None for default
cap: line end style, None for default
join: line join style, None for default
miter_limit: mitre limit, None for default
Returns:
self
'''
self.appearance.subdivlines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit)
return self
def draw(self):
'''
Draw the axes
'''
self.ctx.new_path()
# Get the text height using the selected font. This is used to control text offset and other sizes.
_, self.text_height = Text(self.ctx).of('0', (0, 0)) \
.font(self.appearance.fontparams.font,
self.appearance.fontparams.weight,
self.appearance.fontparams.slant) \
.size(self.appearance.fontparams.size * self.appearance.featurescale) \
.get_size()
self.clip()
self._draw_background()
if self.subdivisons:
self._draw_subdivlines()
self._draw_divlines()
self._draw_axes()
self._draw_axes_values()
self.unclip()
def _draw_background(self):
self.appearance.background.apply(self.ctx)
self.ctx.rectangle(self.position[0], self.position[1], self.width, self.height)
self.ctx.fill()
def _draw_divlines(self):
params = copy.copy(self.appearance.divlines)
params.line_width *= self.appearance.featurescale
params.apply(self.ctx)
for p in self._get_divs(self.start[0], self.extent[0], self.divisions[0]):
self.ctx.move_to(*self.transform_from_graph((p, self.start[1])))
self.ctx.line_to(*self.transform_from_graph((p, self.start[1] + self.extent[1])))
for p in self._get_divs(self.start[1], self.extent[1], self.divisions[1]):
self.ctx.move_to(*self.transform_from_graph((self.start[0], p)))
self.ctx.line_to(*self.transform_from_graph((self.start[0] + self.extent[0], p)))
self.ctx.stroke()
def _draw_subdivlines(self):
params = copy.copy(self.appearance.subdivlines)
params.line_width *= self.appearance.featurescale
params.apply(self.ctx)
for p in self._get_subdivs(self.start[0], self.extent[0], self.divisions[0], self.subdivisionfactor[0]):
self.ctx.move_to(*self.transform_from_graph((p, self.start[1])))
self.ctx.line_to(*self.transform_from_graph((p, self.start[1] + self.extent[1])))
for p in self._get_subdivs(self.start[1], self.extent[1], self.divisions[1], self.subdivisionfactor[1]):
self.ctx.move_to(*self.transform_from_graph((self.start[0], p)))
self.ctx.line_to(*self.transform_from_graph((self.start[0] + self.extent[0], p)))
self.ctx.stroke()
def _draw_axes(self):
params = copy.copy(self.appearance.axislines)
params.line_width *= self.appearance.featurescale
params.apply(self.ctx)
self.ctx.move_to(*self.transform_from_graph((0, self.start[1])))
self.ctx.line_to(*self.transform_from_graph((0, self.start[1] + self.extent[1])))
self.ctx.move_to(*self.transform_from_graph((self.start[0], 0)))
self.ctx.line_to(*self.transform_from_graph((self.start[0] + self.extent[0], 0)))
self.ctx.stroke()
self.ctx.new_path()
self.ctx.arc(*self.transform_from_graph((0, 0)), self.text_height/1.1, 0, 2 * math.pi)
self.ctx.stroke()
def _draw_axes_values(self):
params = copy.copy(self.appearance.axislines)
params.line_width *= self.appearance.featurescale
params.apply(self.ctx)
xoffset = self.text_height/self.appearance.ticklabeloffset
yoffset = self.text_height/self.appearance.ticklabeloffset
for p in self._get_divs(self.start[0], self.extent[0], self.divisions[0]):
if abs(p)>0.001:
position = self.transform_from_graph((p, 0))
pstr = self._format_div(p, self.divisions[0], self.x_div_formatter)
Text(self.ctx).of(pstr, (position[0] - xoffset, position[1] + yoffset)) \
.font(self.appearance.fontparams.font, self.appearance.fontparams.weight,
self.appearance.fontparams.slant) \
.size(self.appearance.fontparams.size*self.appearance.featurescale) \
.align(drawing.RIGHT, drawing.TOP).fill(self.appearance.textcolor)
params.apply(self.ctx)
self.ctx.new_path()
self.ctx.move_to(position[0], position[1])
self.ctx.line_to(position[0], position[1] + yoffset)
self.ctx.stroke()
for p in self._get_divs(self.start[1], self.extent[1], self.divisions[1]):
if abs(p)>0.001:
position = self.transform_from_graph((0, p))
pstr = self._format_div(p, self.divisions[1], self.y_div_formatter)
Text(self.ctx).of(pstr, (position[0] - xoffset, position[1] + yoffset)) \
.font(self.appearance.fontparams.font, self.appearance.fontparams.weight,
self.appearance.fontparams.slant) \
.size(self.appearance.fontparams.size*self.appearance.featurescale) \
.align(drawing.RIGHT, drawing.TOP).fill(self.appearance.textcolor)
params.apply(self.ctx)
self.ctx.new_path()
self.ctx.move_to(position[0], position[1])
self.ctx.line_to(position[0] - xoffset, position[1])
self.ctx.stroke()
def clip(self):
'''
Set the clip region to the axes area.
'''
self.ctx.rectangle(*self.position, self.width, self.height)
self.ctx.save()
self.ctx.clip()
def unclip(self):
'''
Undo a previous clip()
'''
self.ctx.restore()
def _get_divs(self, start, extent, div):
divs = []
n = math.ceil(start/div)*div
while n <= start + extent:
divs.append(n)
n += div
return divs
def _contains(self, values, value, tolerance):
'''
Return true if the sequence values contains the value to within a given tolerance
Args:
values
value
tolerance
Returns:
Result
'''
for v in values:
if abs(value - v) < tolerance:
return True
return False
def _get_subdivs(self, start, extent, div, factor):
subdiv = div/factor
divs = self._get_divs(start, extent, div)
subdivs = []
n = math.ceil(start/subdiv)*subdiv
while n <= start + extent:
if not self._contains(divs, subdiv, extent/100000):
subdivs.append(n)
n += subdiv
return subdivs
def _format_div(self, value, div, formatter):
"""
Formats a division value into a string.
If the division spacing is an integer, the string will be an integer (no dp).
If the division spacing is float, the string will be a float with a suitable number of decimal places
Args:
value: value to be formatted
div: division spacing
formatter: formatting function, accepts vale and div, returns a formatted value string
Returns:
String representation of the value
"""
if formatter:
return formatter(value, div)
if isinstance(value, int):
return str(value)
return str(round(value*1000)/1000)
def transform_from_graph(self, point):
'''
Scale the ctx so that point (x, y) will be correctly positioned in the axes coordinates
Returns:
x, y
'''
x = ((point[0] - self.start[0]) * self.width / self.extent[0]) + self.position[0]
y = self.height + self.position[1] - ((point[1] - self.start[1]) * self.height / self.extent[1])
return x, y
class Plot(Shape):
'''
Plot a function in a set of axes.
'''
def __init__(self, axes):
super().__init__(axes.ctx)
self.axes = axes
self.points = []
self.closed = False
def add(self):
self._do_path_()
first = True
for p in self.points:
if first:
if not self.extend:
self.ctx.move_to(*p)
first = False
else:
self.ctx.line_to(*p)
if self.closed or self.final_close:
self.ctx.close_path()
return self
def stroke(self, pattern=None, line_width=2, dash=None, cap=None, join=None, miter_limit=None):
'''
Stroke overrides the Shape stroke() method. It clips the stroke to the area of the axes. This ensures that if
the curve goes out of range it will not interfere with other parts of the image.
Args:
pattern
line_width
dash
cap
join
miter_limit
Returns:
self
'''
super().stroke(pattern, line_width, dash, cap, join, miter_limit)
def of_function(self, fn, extent=None, precision=100, close=()):
'''
Plot a function y = fn(x)
Args:
fn: the function to plot. It must take a single argument
extent: the range of x values to plot. If not supplied, the plot will use the full range of the axes.
precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added
to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns:
self
'''
self.points = []
start = self.axes.start[0]
end = self.axes.start[0] + self.axes.extent[0]
if extent:
start = max(start, extent[0])
end = min(end, extent[1])
self.points += [self.axes.transform_from_graph((x, fn(x))) for x in np.linspace(start, end, precision)]
if close:
self.points += [self.axes.transform_from_graph(p) for p in close]
self.closed = True
return self
def of_xy_function(self, fn, extent=None, precision=100, close=()):
'''
Plot a function x = fn(y)
Args:
fn: the function to plot. It must take a single argument
extent: the range of y values to plot. If not supplied, the plot will use the full range of the axes.
precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added
to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns:
self
'''
self.points = []
start = self.axes.start[1]
end = self.axes.start[1] + self.axes.extent[0]
if extent:
start = max(start, extent[0])
end = min(end, extent[1])
self.points += [self.axes.transform_from_graph((fn(y), y)) for y in np.linspace(start, end, precision)]
if close:
self.points += [self.axes.transform_from_graph(p) for p in close]
self.closed = True
return self
def of_polar_function(self, fn, extent=(0, 2*math.pi), precision=100, close=()):
'''
Plot a polar function r = fn(theta). theta is measured in radians
Args:
fn: the function to plot. It must take a single argument
extent: the range of theta values to plot. If not supplied, the plot will use the range 0 to 2*pi.
precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added
to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns:
self
'''
self.points = []
for theta in np.linspace(extent[0], extent[1], precision):
r = fn(theta)
self.points.append(self.axes.transform_from_graph((r*math.cos(theta), r*math.sin(theta))))
if close:
self.points += [self.axes.transform_from_graph(p) for p in close]
self.closed = True
return self
def of_parametric_function(self, fx, fy, extent=(0, 1), precision=100, close=()):
'''
Plot a parametric function x = fx(t), y = ft(t).
Args:
fx: x as a function of t. It must take a single argument
fy: y as a function of t. It must take a single argument
extent: the range of t values to plot. If not supplied the range 0 to 1 is used.
precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added
to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns:
self
'''
self.points = []
for t in np.linspace(extent[0], extent[1], precision):
x = fx(t)
y = fy(t)
self.points.append(self.axes.transform_from_graph((x, y)))
if close:
self.points += [self.axes.transform_from_graph(p) for p in close]
self.closed = True
return self
class Scatter:
'''
Plot a scatter chart in a set of axes.
Note that a Scatter plot is not a `Shape` object. It simply draws a scatter plot on the supplied axes.
'''
def __init__(self, axes):
self.axes = axes
self.ctx = axes.ctx
self.stroke_params = StrokeParameters()
self.fill = FillParameters()
self.point_style = POINT_CIRCLE
self.point_size = 4
self.line_style = SCATTER_NO_LINE
def with_line_style(self, style=SCATTER_NO_LINE, pattern=Color(0), line_width=1, dash=None, cap=SQUARE, join=MITER, miter_limit=None):
"""
Outline the shape. This draws the shape to the supplied context.
Parameters are as described for `StrokeParameters`.
Args:
style: SCATTERXXX constant - the style of the plot. Default no line.
pattern: the fill `Pattern` or `Color` to use for the outline, None for default
line_width: width of stroke line. None for default
dash: sequence, dash patter of line. None for default
cap: line end style, None for default.
join: line join style, None for default.
miter_limit: mitre limit, number, None for default
Returns:
self
"""
self.line_style = style
self.stroke_params = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit)
return self
def with_point_style(self, size, style=POINT_CIRCLE, pattern=Color(0), fill_rule=WINDING):
"""
Sets the style of the points
Args:
size: number - the size of the point in user space.
style: POINTXXX constant - the style of the point. Default circular.
pattern: the fill `Pattern` or `Color` to use for the points.
fill_rule: the fill rule to use for the points
Returns:
self
"""
self.point_style = style
self.point_size = size
self.fill = FillParameters(pattern, fill_rule)
return self
def plot(self, x_values, y_values):
'''
Plot a scatter chart of the sample values
Args:
x_values: sequence of numbers - the x values for each sample.
y_values: sequence of numbers - the y values for each sample. The number of x and y values should be equal. If not,
the minimum count will be used. Eg if there are 10 x values and 8 y values, only 8 points will be plotted.
Returns:
self
'''
points = [self.axes.transform_from_graph((x, y)) for x, y in zip(x_values, y_values)]
if self.line_style == SCATTER_CONNECTED:
Polygon(self.ctx).of_points(points).open().stroke(self.stroke_params)
if self.line_style == SCATTER_STALK:
bases = [self.axes.transform_from_graph((x, 0)) for x in x_values]
for p, b in zip(points, bases):
Line(self.ctx).of_start_end(b, p).stroke(self.stroke_params)
for p in points:
Circle(self.ctx).of_center_radius(p, self.point_size).fill(self.fill.pattern, self.fill.fill_rule)
return self
Classes
class Axes (ctx, position, width, height)
-
Controls the range and appearance of a set of Cartesian axes
Expand source code
class Axes: ''' Controls the range and appearance of a set of Cartesian axes ''' def __init__(self, ctx, position, width, height): self.ctx = ctx self.appearance = AxesAppearance() self.position = position self.width = width self.height = height self.start = (0, 0) self.extent = (10, 10) self.divisions = (1, 1) self.subdivisons = False self.subdivisionfactor = (1, 1) self.text_height = 0 self.x_div_formatter = None self.y_div_formatter = None def of_start(self, start): ''' Sets the start value of the axes Args: start: (x, y) value of bottom left corner of axes Returns: self ''' self.start = start return self def of_extent(self, extent): ''' Sets the range of the axes Args: extent: (x, y) range of axes Returns: self ''' self.extent = extent return self def with_feature_scale(self, scale): ''' Sets the scale of the features. For example a value of 2 will make all the gridlines and label text on the axes twice as big. This is a quick way of resizing everything in one go. Args: scale: scale factor Returns: self ''' self.appearance.featurescale = scale return self def with_divisions(self, divisions): ''' Set divisons spacing Args: divisions: (x, y) spacing divisions in each direction Returns: self ''' self.divisions = divisions return self def with_division_formatters(self, x_div_formatter=None, y_div_formatter=None): self.x_div_formatter = x_div_formatter self.y_div_formatter = y_div_formatter return self def with_subdivisions(self, factor): ''' Draw subdivision lines on graph Args: factor: (x, y) Number of subdivisions per division in each direction Returns: self ''' self.subdivisons = True self.subdivisionfactor = factor return self def background(self, pattern): ''' Sets the entire graph background Args: pattern: color or fill pattern Returns: self ''' self.appearance.background = FillParameters(pattern) return self def text_color(self, pattern): ''' Sets the color of the axes text Args: pattern: color or pattern Returns: self ''' self.appearance.textcolor = pattern return self def text_style(self, font="arial", weight=FONT_WEIGHT_BOLD, slant=FONT_SLANT_NORMAL, size=15): ''' Set the style of the axis text Args: font: Font name weight: Font weight slant: Font slant size: Font size in units. This will be multiplied by the featurescale value. Returns: self ''' self.appearance.fontparams = FontParameters(font, weight, slant, size) return self def axis_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None): ''' Sets the style of the axis lines Args: pattern: the fill pattern or color to use for the outline, None for default line_width: width of stroke line, None for default dash: dash patter of line, as for Pycairo, None for default cap: line end style, None for default join: line join style, None for default miter_limit: mitre limit, None for default Returns: self ''' self.appearance.axislines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self def division_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None): ''' Sets the style of the division lines Args: pattern: the fill pattern or color to use for the outline, None for default line_width: width of stroke line, None for default dash: dash patter of line, as for Pycairo, None for default cap: line end style, None for default join: line join style, None for default miter_limit: mitre limit, None for default Returns: self ''' self.appearance.divlines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self def subdivision_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None): ''' Sets the style of the subdivision lines Args: pattern: the fill pattern or color to use for the outline, None for default line_width: width of stroke line, None for default dash: dash patter of line, as for Pycairo, None for default cap: line end style, None for default join: line join style, None for default miter_limit: mitre limit, None for default Returns: self ''' self.appearance.subdivlines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self def draw(self): ''' Draw the axes ''' self.ctx.new_path() # Get the text height using the selected font. This is used to control text offset and other sizes. _, self.text_height = Text(self.ctx).of('0', (0, 0)) \ .font(self.appearance.fontparams.font, self.appearance.fontparams.weight, self.appearance.fontparams.slant) \ .size(self.appearance.fontparams.size * self.appearance.featurescale) \ .get_size() self.clip() self._draw_background() if self.subdivisons: self._draw_subdivlines() self._draw_divlines() self._draw_axes() self._draw_axes_values() self.unclip() def _draw_background(self): self.appearance.background.apply(self.ctx) self.ctx.rectangle(self.position[0], self.position[1], self.width, self.height) self.ctx.fill() def _draw_divlines(self): params = copy.copy(self.appearance.divlines) params.line_width *= self.appearance.featurescale params.apply(self.ctx) for p in self._get_divs(self.start[0], self.extent[0], self.divisions[0]): self.ctx.move_to(*self.transform_from_graph((p, self.start[1]))) self.ctx.line_to(*self.transform_from_graph((p, self.start[1] + self.extent[1]))) for p in self._get_divs(self.start[1], self.extent[1], self.divisions[1]): self.ctx.move_to(*self.transform_from_graph((self.start[0], p))) self.ctx.line_to(*self.transform_from_graph((self.start[0] + self.extent[0], p))) self.ctx.stroke() def _draw_subdivlines(self): params = copy.copy(self.appearance.subdivlines) params.line_width *= self.appearance.featurescale params.apply(self.ctx) for p in self._get_subdivs(self.start[0], self.extent[0], self.divisions[0], self.subdivisionfactor[0]): self.ctx.move_to(*self.transform_from_graph((p, self.start[1]))) self.ctx.line_to(*self.transform_from_graph((p, self.start[1] + self.extent[1]))) for p in self._get_subdivs(self.start[1], self.extent[1], self.divisions[1], self.subdivisionfactor[1]): self.ctx.move_to(*self.transform_from_graph((self.start[0], p))) self.ctx.line_to(*self.transform_from_graph((self.start[0] + self.extent[0], p))) self.ctx.stroke() def _draw_axes(self): params = copy.copy(self.appearance.axislines) params.line_width *= self.appearance.featurescale params.apply(self.ctx) self.ctx.move_to(*self.transform_from_graph((0, self.start[1]))) self.ctx.line_to(*self.transform_from_graph((0, self.start[1] + self.extent[1]))) self.ctx.move_to(*self.transform_from_graph((self.start[0], 0))) self.ctx.line_to(*self.transform_from_graph((self.start[0] + self.extent[0], 0))) self.ctx.stroke() self.ctx.new_path() self.ctx.arc(*self.transform_from_graph((0, 0)), self.text_height/1.1, 0, 2 * math.pi) self.ctx.stroke() def _draw_axes_values(self): params = copy.copy(self.appearance.axislines) params.line_width *= self.appearance.featurescale params.apply(self.ctx) xoffset = self.text_height/self.appearance.ticklabeloffset yoffset = self.text_height/self.appearance.ticklabeloffset for p in self._get_divs(self.start[0], self.extent[0], self.divisions[0]): if abs(p)>0.001: position = self.transform_from_graph((p, 0)) pstr = self._format_div(p, self.divisions[0], self.x_div_formatter) Text(self.ctx).of(pstr, (position[0] - xoffset, position[1] + yoffset)) \ .font(self.appearance.fontparams.font, self.appearance.fontparams.weight, self.appearance.fontparams.slant) \ .size(self.appearance.fontparams.size*self.appearance.featurescale) \ .align(drawing.RIGHT, drawing.TOP).fill(self.appearance.textcolor) params.apply(self.ctx) self.ctx.new_path() self.ctx.move_to(position[0], position[1]) self.ctx.line_to(position[0], position[1] + yoffset) self.ctx.stroke() for p in self._get_divs(self.start[1], self.extent[1], self.divisions[1]): if abs(p)>0.001: position = self.transform_from_graph((0, p)) pstr = self._format_div(p, self.divisions[1], self.y_div_formatter) Text(self.ctx).of(pstr, (position[0] - xoffset, position[1] + yoffset)) \ .font(self.appearance.fontparams.font, self.appearance.fontparams.weight, self.appearance.fontparams.slant) \ .size(self.appearance.fontparams.size*self.appearance.featurescale) \ .align(drawing.RIGHT, drawing.TOP).fill(self.appearance.textcolor) params.apply(self.ctx) self.ctx.new_path() self.ctx.move_to(position[0], position[1]) self.ctx.line_to(position[0] - xoffset, position[1]) self.ctx.stroke() def clip(self): ''' Set the clip region to the axes area. ''' self.ctx.rectangle(*self.position, self.width, self.height) self.ctx.save() self.ctx.clip() def unclip(self): ''' Undo a previous clip() ''' self.ctx.restore() def _get_divs(self, start, extent, div): divs = [] n = math.ceil(start/div)*div while n <= start + extent: divs.append(n) n += div return divs def _contains(self, values, value, tolerance): ''' Return true if the sequence values contains the value to within a given tolerance Args: values value tolerance Returns: Result ''' for v in values: if abs(value - v) < tolerance: return True return False def _get_subdivs(self, start, extent, div, factor): subdiv = div/factor divs = self._get_divs(start, extent, div) subdivs = [] n = math.ceil(start/subdiv)*subdiv while n <= start + extent: if not self._contains(divs, subdiv, extent/100000): subdivs.append(n) n += subdiv return subdivs def _format_div(self, value, div, formatter): """ Formats a division value into a string. If the division spacing is an integer, the string will be an integer (no dp). If the division spacing is float, the string will be a float with a suitable number of decimal places Args: value: value to be formatted div: division spacing formatter: formatting function, accepts vale and div, returns a formatted value string Returns: String representation of the value """ if formatter: return formatter(value, div) if isinstance(value, int): return str(value) return str(round(value*1000)/1000) def transform_from_graph(self, point): ''' Scale the ctx so that point (x, y) will be correctly positioned in the axes coordinates Returns: x, y ''' x = ((point[0] - self.start[0]) * self.width / self.extent[0]) + self.position[0] y = self.height + self.position[1] - ((point[1] - self.start[1]) * self.height / self.extent[1]) return x, y
Methods
def axis_linestyle(self, pattern=<generativepy.color.Color object>, line_width=None, dash=None, cap=None, join=None, miter_limit=None)
-
Sets the style of the axis lines
Args
pattern
- the fill pattern or color to use for the outline, None for default
line_width
- width of stroke line, None for default
dash
- dash patter of line, as for Pycairo, None for default
cap
- line end style, None for default
join
- line join style, None for default
miter_limit
- mitre limit, None for default
Returns
self
Expand source code
def axis_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None): ''' Sets the style of the axis lines Args: pattern: the fill pattern or color to use for the outline, None for default line_width: width of stroke line, None for default dash: dash patter of line, as for Pycairo, None for default cap: line end style, None for default join: line join style, None for default miter_limit: mitre limit, None for default Returns: self ''' self.appearance.axislines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self
def background(self, pattern)
-
Sets the entire graph background
Args
pattern
- color or fill pattern
Returns
self
Expand source code
def background(self, pattern): ''' Sets the entire graph background Args: pattern: color or fill pattern Returns: self ''' self.appearance.background = FillParameters(pattern) return self
def clip(self)
-
Set the clip region to the axes area.
Expand source code
def clip(self): ''' Set the clip region to the axes area. ''' self.ctx.rectangle(*self.position, self.width, self.height) self.ctx.save() self.ctx.clip()
def division_linestyle(self, pattern=<generativepy.color.Color object>, line_width=None, dash=None, cap=None, join=None, miter_limit=None)
-
Sets the style of the division lines
Args
pattern
- the fill pattern or color to use for the outline, None for default
line_width
- width of stroke line, None for default
dash
- dash patter of line, as for Pycairo, None for default
cap
- line end style, None for default
join
- line join style, None for default
miter_limit
- mitre limit, None for default
Returns
self
Expand source code
def division_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None): ''' Sets the style of the division lines Args: pattern: the fill pattern or color to use for the outline, None for default line_width: width of stroke line, None for default dash: dash patter of line, as for Pycairo, None for default cap: line end style, None for default join: line join style, None for default miter_limit: mitre limit, None for default Returns: self ''' self.appearance.divlines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self
def draw(self)
-
Draw the axes
Expand source code
def draw(self): ''' Draw the axes ''' self.ctx.new_path() # Get the text height using the selected font. This is used to control text offset and other sizes. _, self.text_height = Text(self.ctx).of('0', (0, 0)) \ .font(self.appearance.fontparams.font, self.appearance.fontparams.weight, self.appearance.fontparams.slant) \ .size(self.appearance.fontparams.size * self.appearance.featurescale) \ .get_size() self.clip() self._draw_background() if self.subdivisons: self._draw_subdivlines() self._draw_divlines() self._draw_axes() self._draw_axes_values() self.unclip()
def of_extent(self, extent)
-
Sets the range of the axes
Args
extent
- (x, y) range of axes
Returns
self
Expand source code
def of_extent(self, extent): ''' Sets the range of the axes Args: extent: (x, y) range of axes Returns: self ''' self.extent = extent return self
def of_start(self, start)
-
Sets the start value of the axes
Args
start
- (x, y) value of bottom left corner of axes
Returns
self
Expand source code
def of_start(self, start): ''' Sets the start value of the axes Args: start: (x, y) value of bottom left corner of axes Returns: self ''' self.start = start return self
def subdivision_linestyle(self, pattern=<generativepy.color.Color object>, line_width=None, dash=None, cap=None, join=None, miter_limit=None)
-
Sets the style of the subdivision lines
Args
pattern
- the fill pattern or color to use for the outline, None for default
line_width
- width of stroke line, None for default
dash
- dash patter of line, as for Pycairo, None for default
cap
- line end style, None for default
join
- line join style, None for default
miter_limit
- mitre limit, None for default
Returns
self
Expand source code
def subdivision_linestyle(self, pattern=Color(0), line_width=None, dash=None, cap=None, join=None, miter_limit=None): ''' Sets the style of the subdivision lines Args: pattern: the fill pattern or color to use for the outline, None for default line_width: width of stroke line, None for default dash: dash patter of line, as for Pycairo, None for default cap: line end style, None for default join: line join style, None for default miter_limit: mitre limit, None for default Returns: self ''' self.appearance.subdivlines = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self
def text_color(self, pattern)
-
Sets the color of the axes text
Args
pattern
- color or pattern
Returns
self
Expand source code
def text_color(self, pattern): ''' Sets the color of the axes text Args: pattern: color or pattern Returns: self ''' self.appearance.textcolor = pattern return self
def text_style(self, font='arial', weight=1, slant=0, size=15)
-
Set the style of the axis text
Args
font
- Font name
weight
- Font weight
slant
- Font slant
size
- Font size in units. This will be multiplied by the featurescale value.
Returns
self
Expand source code
def text_style(self, font="arial", weight=FONT_WEIGHT_BOLD, slant=FONT_SLANT_NORMAL, size=15): ''' Set the style of the axis text Args: font: Font name weight: Font weight slant: Font slant size: Font size in units. This will be multiplied by the featurescale value. Returns: self ''' self.appearance.fontparams = FontParameters(font, weight, slant, size) return self
def transform_from_graph(self, point)
-
Scale the ctx so that point (x, y) will be correctly positioned in the axes coordinates
Returns
x, y
Expand source code
def transform_from_graph(self, point): ''' Scale the ctx so that point (x, y) will be correctly positioned in the axes coordinates Returns: x, y ''' x = ((point[0] - self.start[0]) * self.width / self.extent[0]) + self.position[0] y = self.height + self.position[1] - ((point[1] - self.start[1]) * self.height / self.extent[1]) return x, y
def unclip(self)
-
Undo a previous clip()
Expand source code
def unclip(self): ''' Undo a previous clip() ''' self.ctx.restore()
def with_division_formatters(self, x_div_formatter=None, y_div_formatter=None)
-
Expand source code
def with_division_formatters(self, x_div_formatter=None, y_div_formatter=None): self.x_div_formatter = x_div_formatter self.y_div_formatter = y_div_formatter return self
def with_divisions(self, divisions)
-
Set divisons spacing
Args
divisions
- (x, y) spacing divisions in each direction
Returns
self
Expand source code
def with_divisions(self, divisions): ''' Set divisons spacing Args: divisions: (x, y) spacing divisions in each direction Returns: self ''' self.divisions = divisions return self
def with_feature_scale(self, scale)
-
Sets the scale of the features. For example a value of 2 will make all the gridlines and label text on the axes twice as big. This is a quick way of resizing everything in one go.
Args
scale
- scale factor
Returns
self
Expand source code
def with_feature_scale(self, scale): ''' Sets the scale of the features. For example a value of 2 will make all the gridlines and label text on the axes twice as big. This is a quick way of resizing everything in one go. Args: scale: scale factor Returns: self ''' self.appearance.featurescale = scale return self
def with_subdivisions(self, factor)
-
Draw subdivision lines on graph
Args
factor
- (x, y) Number of subdivisions per division in each direction
Returns
self
Expand source code
def with_subdivisions(self, factor): ''' Draw subdivision lines on graph Args: factor: (x, y) Number of subdivisions per division in each direction Returns: self ''' self.subdivisons = True self.subdivisionfactor = factor return self
class AxesAppearance
-
Parameters that control the appearance of the axes (colours, line styles).
Expand source code
@dataclass class AxesAppearance: ''' Parameters that control the appearance of the axes (colours, line styles). ''' background = FillParameters(Color(1)) textcolor = Color(0.2) fontparams = FontParameters('arial', size=15, weight=FONT_WEIGHT_BOLD) divlines = StrokeParameters(Color(0.8, 0.8, 1), line_width=2, cap=BUTT) subdivlines = StrokeParameters(Color(0.9, 0.9, 1), line_width=2, cap=BUTT) axislines = StrokeParameters(Color(0.2), line_width=2, cap=BUTT) featurescale = 1 # x and y offset of tick labels. The actual offset is: # text height (height of 0 character using fontparams) DIVIDED by ticklabeloffset ticklabeloffset = 1.1
Class variables
var axislines
var background
var divlines
var featurescale
var fontparams
var subdivlines
var textcolor
var ticklabeloffset
class Plot (axes)
-
Plot a function in a set of axes.
Args
ctx
- Pycairo drawing context - The context to draw on.
Returns
self
Expand source code
class Plot(Shape): ''' Plot a function in a set of axes. ''' def __init__(self, axes): super().__init__(axes.ctx) self.axes = axes self.points = [] self.closed = False def add(self): self._do_path_() first = True for p in self.points: if first: if not self.extend: self.ctx.move_to(*p) first = False else: self.ctx.line_to(*p) if self.closed or self.final_close: self.ctx.close_path() return self def stroke(self, pattern=None, line_width=2, dash=None, cap=None, join=None, miter_limit=None): ''' Stroke overrides the Shape stroke() method. It clips the stroke to the area of the axes. This ensures that if the curve goes out of range it will not interfere with other parts of the image. Args: pattern line_width dash cap join miter_limit Returns: self ''' super().stroke(pattern, line_width, dash, cap, join, miter_limit) def of_function(self, fn, extent=None, precision=100, close=()): ''' Plot a function y = fn(x) Args: fn: the function to plot. It must take a single argument extent: the range of x values to plot. If not supplied, the plot will use the full range of the axes. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] start = self.axes.start[0] end = self.axes.start[0] + self.axes.extent[0] if extent: start = max(start, extent[0]) end = min(end, extent[1]) self.points += [self.axes.transform_from_graph((x, fn(x))) for x in np.linspace(start, end, precision)] if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self def of_xy_function(self, fn, extent=None, precision=100, close=()): ''' Plot a function x = fn(y) Args: fn: the function to plot. It must take a single argument extent: the range of y values to plot. If not supplied, the plot will use the full range of the axes. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] start = self.axes.start[1] end = self.axes.start[1] + self.axes.extent[0] if extent: start = max(start, extent[0]) end = min(end, extent[1]) self.points += [self.axes.transform_from_graph((fn(y), y)) for y in np.linspace(start, end, precision)] if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self def of_polar_function(self, fn, extent=(0, 2*math.pi), precision=100, close=()): ''' Plot a polar function r = fn(theta). theta is measured in radians Args: fn: the function to plot. It must take a single argument extent: the range of theta values to plot. If not supplied, the plot will use the range 0 to 2*pi. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] for theta in np.linspace(extent[0], extent[1], precision): r = fn(theta) self.points.append(self.axes.transform_from_graph((r*math.cos(theta), r*math.sin(theta)))) if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self def of_parametric_function(self, fx, fy, extent=(0, 1), precision=100, close=()): ''' Plot a parametric function x = fx(t), y = ft(t). Args: fx: x as a function of t. It must take a single argument fy: y as a function of t. It must take a single argument extent: the range of t values to plot. If not supplied the range 0 to 1 is used. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] for t in np.linspace(extent[0], extent[1], precision): x = fx(t) y = fy(t) self.points.append(self.axes.transform_from_graph((x, y))) if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self
Ancestors
Methods
def of_function(self, fn, extent=None, precision=100, close=())
-
Plot a function y = fn(x)
Args
fn
- the function to plot. It must take a single argument
extent
- the range of x values to plot. If not supplied, the plot will use the full range of the axes.
precision
- number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close
- sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns
self
Expand source code
def of_function(self, fn, extent=None, precision=100, close=()): ''' Plot a function y = fn(x) Args: fn: the function to plot. It must take a single argument extent: the range of x values to plot. If not supplied, the plot will use the full range of the axes. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] start = self.axes.start[0] end = self.axes.start[0] + self.axes.extent[0] if extent: start = max(start, extent[0]) end = min(end, extent[1]) self.points += [self.axes.transform_from_graph((x, fn(x))) for x in np.linspace(start, end, precision)] if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self
def of_parametric_function(self, fx, fy, extent=(0, 1), precision=100, close=())
-
Plot a parametric function x = fx(t), y = ft(t).
Args
fx
- x as a function of t. It must take a single argument
fy
- y as a function of t. It must take a single argument
extent
- the range of t values to plot. If not supplied the range 0 to 1 is used.
precision
- number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close
- sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns
self
Expand source code
def of_parametric_function(self, fx, fy, extent=(0, 1), precision=100, close=()): ''' Plot a parametric function x = fx(t), y = ft(t). Args: fx: x as a function of t. It must take a single argument fy: y as a function of t. It must take a single argument extent: the range of t values to plot. If not supplied the range 0 to 1 is used. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] for t in np.linspace(extent[0], extent[1], precision): x = fx(t) y = fy(t) self.points.append(self.axes.transform_from_graph((x, y))) if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self
def of_polar_function(self, fn, extent=(0, 6.283185307179586), precision=100, close=())
-
Plot a polar function r = fn(theta). theta is measured in radians
Args
fn
- the function to plot. It must take a single argument
extent
- the range of theta values to plot. If not supplied, the plot will use the range 0 to 2*pi.
precision
- number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close
- sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns
self
Expand source code
def of_polar_function(self, fn, extent=(0, 2*math.pi), precision=100, close=()): ''' Plot a polar function r = fn(theta). theta is measured in radians Args: fn: the function to plot. It must take a single argument extent: the range of theta values to plot. If not supplied, the plot will use the range 0 to 2*pi. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] for theta in np.linspace(extent[0], extent[1], precision): r = fn(theta) self.points.append(self.axes.transform_from_graph((r*math.cos(theta), r*math.sin(theta)))) if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self
def of_xy_function(self, fn, extent=None, precision=100, close=())
-
Plot a function x = fn(y)
Args
fn
- the function to plot. It must take a single argument
extent
- the range of y values to plot. If not supplied, the plot will use the full range of the axes.
precision
- number of points to plot. Defaults to 100. This can be increased if needed for hi res plots
close
- sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled.
Returns
self
Expand source code
def of_xy_function(self, fn, extent=None, precision=100, close=()): ''' Plot a function x = fn(y) Args: fn: the function to plot. It must take a single argument extent: the range of y values to plot. If not supplied, the plot will use the full range of the axes. precision: number of points to plot. Defaults to 100. This can be increased if needed for hi res plots close: sequence of (x, y) points. One or more additional points, defined in axes coordinates, that will be added to the plot path to create a polygon. The polygon will also be closed. This allows an area under the curve to be filled. Returns: self ''' self.points = [] start = self.axes.start[1] end = self.axes.start[1] + self.axes.extent[0] if extent: start = max(start, extent[0]) end = min(end, extent[1]) self.points += [self.axes.transform_from_graph((fn(y), y)) for y in np.linspace(start, end, precision)] if close: self.points += [self.axes.transform_from_graph(p) for p in close] self.closed = True return self
def stroke(self, pattern=None, line_width=2, dash=None, cap=None, join=None, miter_limit=None)
-
Stroke overrides the Shape stroke() method. It clips the stroke to the area of the axes. This ensures that if the curve goes out of range it will not interfere with other parts of the image.
Args
pattern line_width dash cap join miter_limit
Returns
self
Expand source code
def stroke(self, pattern=None, line_width=2, dash=None, cap=None, join=None, miter_limit=None): ''' Stroke overrides the Shape stroke() method. It clips the stroke to the area of the axes. This ensures that if the curve goes out of range it will not interfere with other parts of the image. Args: pattern line_width dash cap join miter_limit Returns: self ''' super().stroke(pattern, line_width, dash, cap, join, miter_limit)
Inherited members
class Scatter (axes)
-
Plot a scatter chart in a set of axes. Note that a Scatter plot is not a
Shape
object. It simply draws a scatter plot on the supplied axes.Expand source code
class Scatter: ''' Plot a scatter chart in a set of axes. Note that a Scatter plot is not a `Shape` object. It simply draws a scatter plot on the supplied axes. ''' def __init__(self, axes): self.axes = axes self.ctx = axes.ctx self.stroke_params = StrokeParameters() self.fill = FillParameters() self.point_style = POINT_CIRCLE self.point_size = 4 self.line_style = SCATTER_NO_LINE def with_line_style(self, style=SCATTER_NO_LINE, pattern=Color(0), line_width=1, dash=None, cap=SQUARE, join=MITER, miter_limit=None): """ Outline the shape. This draws the shape to the supplied context. Parameters are as described for `StrokeParameters`. Args: style: SCATTERXXX constant - the style of the plot. Default no line. pattern: the fill `Pattern` or `Color` to use for the outline, None for default line_width: width of stroke line. None for default dash: sequence, dash patter of line. None for default cap: line end style, None for default. join: line join style, None for default. miter_limit: mitre limit, number, None for default Returns: self """ self.line_style = style self.stroke_params = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self def with_point_style(self, size, style=POINT_CIRCLE, pattern=Color(0), fill_rule=WINDING): """ Sets the style of the points Args: size: number - the size of the point in user space. style: POINTXXX constant - the style of the point. Default circular. pattern: the fill `Pattern` or `Color` to use for the points. fill_rule: the fill rule to use for the points Returns: self """ self.point_style = style self.point_size = size self.fill = FillParameters(pattern, fill_rule) return self def plot(self, x_values, y_values): ''' Plot a scatter chart of the sample values Args: x_values: sequence of numbers - the x values for each sample. y_values: sequence of numbers - the y values for each sample. The number of x and y values should be equal. If not, the minimum count will be used. Eg if there are 10 x values and 8 y values, only 8 points will be plotted. Returns: self ''' points = [self.axes.transform_from_graph((x, y)) for x, y in zip(x_values, y_values)] if self.line_style == SCATTER_CONNECTED: Polygon(self.ctx).of_points(points).open().stroke(self.stroke_params) if self.line_style == SCATTER_STALK: bases = [self.axes.transform_from_graph((x, 0)) for x in x_values] for p, b in zip(points, bases): Line(self.ctx).of_start_end(b, p).stroke(self.stroke_params) for p in points: Circle(self.ctx).of_center_radius(p, self.point_size).fill(self.fill.pattern, self.fill.fill_rule) return self
Methods
def plot(self, x_values, y_values)
-
Plot a scatter chart of the sample values
Args
x_values
- sequence of numbers - the x values for each sample.
y_values
- sequence of numbers - the y values for each sample. The number of x and y values should be equal. If not,
the minimum count will be used. Eg if there are 10 x values and 8 y values, only 8 points will be plotted.
Returns
self
Expand source code
def plot(self, x_values, y_values): ''' Plot a scatter chart of the sample values Args: x_values: sequence of numbers - the x values for each sample. y_values: sequence of numbers - the y values for each sample. The number of x and y values should be equal. If not, the minimum count will be used. Eg if there are 10 x values and 8 y values, only 8 points will be plotted. Returns: self ''' points = [self.axes.transform_from_graph((x, y)) for x, y in zip(x_values, y_values)] if self.line_style == SCATTER_CONNECTED: Polygon(self.ctx).of_points(points).open().stroke(self.stroke_params) if self.line_style == SCATTER_STALK: bases = [self.axes.transform_from_graph((x, 0)) for x in x_values] for p, b in zip(points, bases): Line(self.ctx).of_start_end(b, p).stroke(self.stroke_params) for p in points: Circle(self.ctx).of_center_radius(p, self.point_size).fill(self.fill.pattern, self.fill.fill_rule) return self
def with_line_style(self, style=0, pattern=<generativepy.color.Color object>, line_width=1, dash=None, cap=4, join=0, miter_limit=None)
-
Outline the shape. This draws the shape to the supplied context.
Parameters are as described for
StrokeParameters
.Args
style
- SCATTERXXX constant - the style of the plot. Default no line.
pattern
- the fill
Pattern
orColor
to use for the outline, None for default line_width
- width of stroke line. None for default
dash
- sequence, dash patter of line. None for default
cap
- line end style, None for default.
join
- line join style, None for default.
miter_limit
- mitre limit, number, None for default
Returns
self
Expand source code
def with_line_style(self, style=SCATTER_NO_LINE, pattern=Color(0), line_width=1, dash=None, cap=SQUARE, join=MITER, miter_limit=None): """ Outline the shape. This draws the shape to the supplied context. Parameters are as described for `StrokeParameters`. Args: style: SCATTERXXX constant - the style of the plot. Default no line. pattern: the fill `Pattern` or `Color` to use for the outline, None for default line_width: width of stroke line. None for default dash: sequence, dash patter of line. None for default cap: line end style, None for default. join: line join style, None for default. miter_limit: mitre limit, number, None for default Returns: self """ self.line_style = style self.stroke_params = StrokeParameters(pattern, line_width, dash, cap, join, miter_limit) return self
def with_point_style(self, size, style=0, pattern=<generativepy.color.Color object>, fill_rule=1)
-
Sets the style of the points
Args
size
- number - the size of the point in user space.
style
- POINTXXX constant - the style of the point. Default circular.
pattern
- the fill
Pattern
orColor
to use for the points. fill_rule
- the fill rule to use for the points
Returns
self
Expand source code
def with_point_style(self, size, style=POINT_CIRCLE, pattern=Color(0), fill_rule=WINDING): """ Sets the style of the points Args: size: number - the size of the point in user space. style: POINTXXX constant - the style of the point. Default circular. pattern: the fill `Pattern` or `Color` to use for the points. fill_rule: the fill rule to use for the points Returns: self """ self.point_style = style self.point_size = size self.fill = FillParameters(pattern, fill_rule) return self