Skip to content

Commit

Permalink
Version 0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
konstantint committed Oct 11, 2012
0 parents commit e1f2030
Show file tree
Hide file tree
Showing 8 changed files with 804 additions and 0 deletions.
61 changes: 61 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Venn diagram plotting routines for Python/Matplotlib
====================================================

About
-----
This package contains a rountine for plotting area-weighted three-circle venn diagrams.

Copyright 2012, Konstantin Tretyakov.
http://kt.era.ee/

Licensed under BSD.


Installation
------------
Installable as any other Python package, either through :code:`easy_install`, or through :code:`python setup.py install`, or by simply including the package to :code:`sys.path`.

Usage
-----
There are two main functions in the package: :code:`venn3_circles` and :code:`venn3`

Both accept as their only required argument an 8-element list of set sizes,

:code:`sets = (abc, Abc, aBc, ABc, abC, AbC, aBC, ABC)`

That is, for example, :code:`sets[1]` contains the size of the set (A and not B and not C),
:code:`sets[3]` contains the size of the set (A and B and not C), etc. Note that the value in :code:`sets[0]` is not used.

The function :code:`venn3_circles` simply draws three circles such that their intersection areas would correspond
to the desired set intersection sizes. Note that it is not impossible to achieve exact correspondence, but in
most cases the picture will still provide a decent indication.

The function :code:`venn3` draws the venn diagram as a collection of 8 separate colored patches with text labels.

The function :code:`venn3_circles` returns the list of Circle patches that may be tuned further.
The function :code:`venn3` returns an object of class :code:`Venn3`, which also gives access to diagram patches and text elements.

Basic Example::
from matplotlib.venn import venn3
venn3(sets = (0, 1, 1, 1, 2, 1, 2, 2), set_labels = ('Set1', 'Set2', 'Set3'))
More elaborate example::

from matplotlib import pyplot as plt
import numpy as np
from matplotlib.venn import venn3, venn3_circles
plt.figure(figsize=(4,4))
v = venn3(sets=(0, 1, 1, 1, 1, 1, 1, 1), set_labels = ('A', 'B', 'C'))
v.get_patch_by_id('100').set_alpha(1.0)
v.get_patch_by_id('100').set_color('white')
v.get_text_by_id('100').set_text('Unknown')
v.labels[0].set_text('Set "A"')
c = venn3_circles(sets=(0, 1, 1, 1, 1, 1, 1, 1), linestyle='dashed')
c[0].set_lw(1.0)
c[0].set_ls('dotted')
plt.title("Sample Venn diagram")
plt.annotate('Unknown set', xy=v.get_text_by_id('100').get_position() - np.array([0, 0.05]), xytext=(-70,-70),
ha='center', textcoords='offset points', bbox=dict(boxstyle='round,pad=0.5', fc='gray', alpha=0.1),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.5',color='gray'))

5 changes: 5 additions & 0 deletions matplotlib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Namespace package
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
54 changes: 54 additions & 0 deletions matplotlib/venn/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'''
Venn diagram plotting routines.
Copyright 2012, Konstantin Tretyakov.
http://kt.era.ee/
Licensed under BSD.
This package contains a rountine for plotting area-weighted three-circle venn diagrams.
There are two main functions here: venn3_circles and venn3
Both accept as their only required argument an 8-element list of set sizes,
sets = (abc, Abc, aBc, ABc, abC, AbC, aBC, ABC)
That is, for example, sets[1] contains the size of the set (A and not B and not C),
sets[3] contains the size of the set (A and B and not C), etc. Note that the value in sets[0] is not used.
The function venn3_circles simply draws three circles such that their intersection areas would correspond
to the desired set intersection sizes. Note that it is not impossible to achieve exact correspondence, but in
most cases the picture will still provide a decent indication.
The function venn3 draws the venn diagram as a collection of 8 separate colored patches with text labels.
The function venn3_circles returns the list of Circle patches that may be tuned further.
The function venn3 returns an object of class Venn3, which also gives access to diagram patches and text elements.
Example:
from matplotlib import pyplot as plt
import numpy as np
from matplotlib.venn import venn3, venn3_circles
plt.figure(figsize=(4,4))
v = venn3(sets=(0, 1, 1, 1, 1, 1, 1, 1), set_labels = ('A', 'B', 'C'))
v.get_patch_by_id('100').set_alpha(1.0)
v.get_patch_by_id('100').set_color('white')
v.get_text_by_id('100').set_text('Unknown')
v.labels[0].set_text('Set "A"')
c = venn3_circles(sets=(0, 1, 1, 1, 1, 1, 1, 1), linestyle='dashed')
c[0].set_lw(1.0)
c[0].set_ls('dotted')
plt.title("Sample Venn diagram")
plt.annotate('Unknown set', xy=v.get_text_by_id('100').get_position() - np.array([0, 0.05]), xytext=(-70,-70),
ha='center', textcoords='offset points', bbox=dict(boxstyle='round,pad=0.5', fc='gray', alpha=0.1),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.5',color='gray'))
'''
from matplotlib.venn import *
v = venn3(sets=(0, 1, 1, 1, 1, 1, 1, 1), set_labels = ('A', 'B', 'C'))
venn3_circles(sets=(0, 1, 1, 1, 1, 1, 1, 1), linestyle='dashed')
v.get_patch_by_id('100').set_alpha(1.0)
v.get_patch_by_id('100').set_color('white')
v.get_text_by_id('100').set_text('Unknown')


from _venn3 import venn3, venn3_circles
___all___ = ['venn3', 'venn3_circles']
156 changes: 156 additions & 0 deletions matplotlib/venn/_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
'''
Venn diagram plotting routines.
Math helper functions.
Copyright 2012, Konstantin Tretyakov.
http://kt.era.ee/
Licensed under BSD.
'''

from scipy.optimize import brentq
import numpy as np

tol = 1e-10

def circle_intersection_area(r, R, d):
'''
Formula from: http://mathworld.wolfram.com/Circle-CircleIntersection.html
Does not make sense for negative r, R or d
>>> circle_intersection_area(0.0, 0.0, 0.0)
0.0
>>> circle_intersection_area(1.0, 1.0, 0.0)
3.1415...
>>> circle_intersection_area(1.0, 1.0, 1.0)
1.2283...
'''
if np.abs(d) < tol:
minR = np.min([r, R])
return np.pi * minR**2
if np.abs(r - 0) < tol or np.abs(R - 0) < tol:
return 0.0
d2, r2, R2 = float(d**2), float(r**2), float(R**2)
arg = (d2 + r2 - R2)/2/d/r
arg = np.max([np.min([arg, 1.0]), -1.0]) # Even with valid arguments, the above computation may result in things like -1.001
A = r2 * np.arccos(arg)
arg = (d2 + R2 - r2)/2/d/R
arg = np.max([np.min([arg, 1.0]), -1.0])
B = R2 * np.arccos(arg)
arg = (-d+r+R)*(d+r-R)*(d-r+R)*(d+r+R)
arg = np.max([arg, 0])
C = -0.5*np.sqrt(arg)
return A + B + C

def circle_line_intersection(center, r, a, b):
'''
Computes two intersection points between the circle centered at <center> and radius <r> and a line given by two points a and b.
If no intersection exists, or if a==b, None is returned. If one intersection exists, it is repeated in the answer.
>>> circle_line_intersection(np.array([0.0, 0.0]), 1, np.array([-1.0, 0.0]), np.array([1.0, 0.0]))
array([[ 1., 0.],
[-1., 0.]])
>>> np.round(circle_line_intersection(np.array([1.0, 1.0]), np.sqrt(2), np.array([-1.0, 1.0]), np.array([1.0, -1.0])), 6)
array([[ 0., 0.],
[ 0., 0.]])
'''
s = b - a
# Quadratic eqn coefs
A = np.linalg.norm(s)**2
if abs(A) < tol:
return None
B = 2*np.dot(a-center, s)
C = np.linalg.norm(a-center)**2 - r**2
disc = B**2 - 4*A*C
if disc < 0.0:
return None
t1 = (-B + np.sqrt(disc))/2.0/A
t2 = (-B - np.sqrt(disc))/2.0/A
return np.array([a + t1*s, a+t2*s])

def find_distance_by_area(r, R, a):
'''
Solves circle_intersection_area(r, R, d) == a for d numerically (analytical solution seems to be too ugly to pursue).
Assumes that a < pi * min(r, R)**2, will fail otherwise.
>>> find_distance_by_area(1, 1, 0)
2.0
>>> round(find_distance_by_area(1, 1, 3.1415), 4)
0.0
>>> d = find_distance_by_area(2, 3, 4)
>>> d
3.37...
>>> round(circle_intersection_area(2, 3, d), 10)
4.0
'''
if r > R:
r, R = R, r
if np.abs(a) < tol:
return float(r + R)
if np.abs(min([r, R])**2*np.pi - a) < tol:
return np.abs(R - r)
return brentq(lambda x: circle_intersection_area(r, R, x) - a, R-r, R+r)

def circle_circle_intersection(C_a, r_a, C_b, r_b):
'''
Finds the coordinates of the intersection points of two circles A and B.
Circle center coordinates C_a and C_b, should be given as tuples (or 1x2 arrays).
Returns a 2x2 array result with result[0] being the first intersection point (to the right of the vector C_a -> C_b)
and result[1] being the second intersection point.
If there is a single intersection point, it is repeated in output.
If there are no intersection points or an infinite number of those, None is returned.
>>> circle_circle_intersection([0, 0], 1, [1, 0], 1) # Two intersection points
array([[ 0.5 , -0.866...],
[ 0.5 , 0.866...]])
>>> circle_circle_intersection([0, 0], 1, [2, 0], 1) # Single intersection point (circles touch from outside)
array([[ 1., 0.],
[ 1., 0.]])
>>> circle_circle_intersection([0, 0], 1, [0.5, 0], 0.5) # Single intersection point (circles touch from inside)
array([[ 1., 0.],
[ 1., 0.]])
>>> circle_circle_intersection([0, 0], 1, [0, 0], 1) is None # Infinite number of intersections (circles coincide)
True
>>> circle_circle_intersection([0, 0], 1, [0, 0.1], 0.8) is None # No intersections (one circle inside another)
True
>>> circle_circle_intersection([0, 0], 1, [2.1, 0], 1) is None # No intersections (one circle outside another)
True
'''
C_a, C_b = np.array(C_a, float), np.array(C_b, float)
v_ab = C_b - C_a
d_ab = np.linalg.norm(v_ab)
if np.abs(d_ab) < tol: # No intersection points
return None
cos_gamma = (d_ab**2 + r_a**2 - r_b**2)/2.0/d_ab/r_a
if abs(cos_gamma) > 1.0:
return None
sin_gamma = np.sqrt(1 - cos_gamma**2)
u = v_ab / d_ab
v = np.array([-u[1], u[0]])
pt1 = C_a + r_a * cos_gamma*u - r_a * sin_gamma*v
pt2 = C_a + r_a * cos_gamma*u + r_a * sin_gamma*v
return np.array([pt1, pt2])

def vector_angle_in_degrees(v):
'''
Given a vector, returns its elevation angle in degrees (-180..180).
>>> vector_angle_in_degrees([1, 0])
0.0
>>> vector_angle_in_degrees([1, 1])
45.0
>>> vector_angle_in_degrees([0, 1])
90.0
>>> vector_angle_in_degrees([-1, 1])
135.0
>>> vector_angle_in_degrees([-1, 0])
180.0
>>> vector_angle_in_degrees([-1, -1])
-135.0
>>> vector_angle_in_degrees([0, -1])
-90.0
>>> vector_angle_in_degrees([1, -1])
-45.0
'''
return np.arctan2(v[1], v[0])*180/np.pi
Loading

0 comments on commit e1f2030

Please sign in to comment.