Giter Club home page Giter Club logo

affine's Introduction

Rasterio

Rasterio reads and writes geospatial raster data.

image

image

image

image

Geographic information systems use GeoTIFF and other formats to organize and store gridded, or raster, datasets. Rasterio reads and writes these formats and provides a Python API based on N-D arrays.

Rasterio 1.4 works with Python 3.9+, Numpy 1.21+, and GDAL 3.3+. Official binary packages for Linux, macOS, and Windows with most built-in format drivers plus HDF5, netCDF, and OpenJPEG2000 are available on PyPI.

Read the documentation for more details: https://rasterio.readthedocs.io/.

Example

Here's an example of some basic features that Rasterio provides. Three bands are read from an image and averaged to produce something like a panchromatic band. This new band is then written to a new single band TIFF.

import numpy as np
import rasterio

# Read raster bands directly to Numpy arrays.
#
with rasterio.open('tests/data/RGB.byte.tif') as src:
    r, g, b = src.read()

# Combine arrays in place. Expecting that the sum will
# temporarily exceed the 8-bit integer range, initialize it as
# a 64-bit float (the numpy default) array. Adding other
# arrays to it in-place converts those arrays "up" and
# preserves the type of the total array.
total = np.zeros(r.shape)

for band in r, g, b:
    total += band

total /= 3

# Write the product as a raster band to a new 8-bit file. For
# the new file's profile, we start with the meta attributes of
# the source file, but then change the band count to 1, set the
# dtype to uint8, and specify LZW compression.
profile = src.profile
profile.update(dtype=rasterio.uint8, count=1, compress='lzw')

with rasterio.open('example-total.tif', 'w', **profile) as dst:
    dst.write(total.astype(rasterio.uint8), 1)

The output:

image

API Overview

Rasterio gives access to properties of a geospatial raster file.

with rasterio.open('tests/data/RGB.byte.tif') as src:
    print(src.width, src.height)
    print(src.crs)
    print(src.transform)
    print(src.count)
    print(src.indexes)

# Printed:
# (791, 718)
# {u'units': u'm', u'no_defs': True, u'ellps': u'WGS84', u'proj': u'utm', u'zone': 18}
# Affine(300.0379266750948, 0.0, 101985.0,
#        0.0, -300.041782729805, 2826915.0)
# 3
# [1, 2, 3]

A rasterio dataset also provides methods for getting read/write windows (like extended array slices) given georeferenced coordinates.

with rasterio.open('tests/data/RGB.byte.tif') as src:
    window = src.window(*src.bounds)
    print(window)
    print(src.read(window=window).shape)

# Printed:
# Window(col_off=0.0, row_off=0.0, width=791.0000000000002, height=718.0)
# (3, 718, 791)

Rasterio CLI

Rasterio's command line interface, named "rio", is documented at cli.rst. Its rio insp command opens the hood of any raster dataset so you can poke around using Python.

$ rio insp tests/data/RGB.byte.tif
Rasterio 0.10 Interactive Inspector (Python 3.4.1)
Type "src.meta", "src.read(1)", or "help(src)" for more information.
>>> src.name
'tests/data/RGB.byte.tif'
>>> src.closed
False
>>> src.shape
(718, 791)
>>> src.crs
{'init': 'epsg:32618'}
>>> b, g, r = src.read()
>>> b
masked_array(data =
 [[-- -- -- ..., -- -- --]
 [-- -- -- ..., -- -- --]
 [-- -- -- ..., -- -- --]
 ...,
 [-- -- -- ..., -- -- --]
 [-- -- -- ..., -- -- --]
 [-- -- -- ..., -- -- --]],
             mask =
 [[ True  True  True ...,  True  True  True]
 [ True  True  True ...,  True  True  True]
 [ True  True  True ...,  True  True  True]
 ...,
 [ True  True  True ...,  True  True  True]
 [ True  True  True ...,  True  True  True]
 [ True  True  True ...,  True  True  True]],
       fill_value = 0)

>>> np.nanmin(b), np.nanmax(b), np.nanmean(b)
(0, 255, 29.94772668847656)

Rio Plugins

Rio provides the ability to create subcommands using plugins. See cli.rst for more information on building plugins.

See the plugin registry for a list of available plugins.

Installation

See docs/installation.rst

Support

The primary forum for questions about installation and usage of Rasterio is https://rasterio.groups.io/g/main. The authors and other users will answer questions when they have expertise to share and time to explain. Please take the time to craft a clear question and be patient about responses.

Please do not bring these questions to Rasterio's issue tracker, which we want to reserve for bug reports and other actionable issues.

Development and Testing

See CONTRIBUTING.rst.

Documentation

See docs/.

License

See LICENSE.txt.

Authors

The rasterio project was begun at Mapbox and was transferred to the rasterio Github organization in October 2021.

See AUTHORS.txt.

Changes

See CHANGES.txt.

Who is Using Rasterio?

See here.

affine's People

Contributors

astrojuanlu avatar drnextgis avatar geowurster avatar groutr avatar kirill888 avatar loicdtx avatar mwtoews avatar rotzbua avatar sgillies avatar smr547 avatar stuaxo avatar vincentsarago avatar wessm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

affine's Issues

Generate manylinux WHLs

For developers using docker in more restricted environments having manylinux WHLs available would be good.

2.0 release

  • Removing global precision (#20).
  • Changing handedness of coordinate system so that rotations are counter-clockwise (#25), a better fit for geospatial applications.

2.3.0 release

We will have a 2.3 release to announce the deprecation of right multiplication like (1, 1) * Affine.scale(2) (see #32).

2.1 release

With the new feature from #28. Target date: 2017-06-26.

Blocked by #30.

Affine class no longer compatible with delayed operations in Dask 2022.8.1

A recent change (dask/dask#9361) in how dask handles arguments that are instances or subclasses of namedtuple prevents Affine objects from being passed to delayed operations such as dask.array.map_blocks. Dask now unpacks the namedtuples and then tries to repack them before providing them to the delayed function at compute time. This breaks for Affine objects since they unpack to the full 9 coefficients but Affine.__new__ only takes the first 6 coefficients. The result is

TypeError: __new__() takes 7 positional arguments but 10 were given

when dask graphs are computed.

My current solution is to pass the coefficients in place of the instance but it would be great to simply pass Affine objects again.

Associativity in transform?

Hi,

I've been working with the GUF recently, and have been making heavy uses of windows & transforms, so forgive me if this I'm just new or this is anticipated.

I'm in a situation where I can make a view of the GUF (a 2.8 arcsecond global raster, here defined in a vrt)

import rasterio as rio
handler = rio.open('/mnt/data/guf/GUF28_v2/GUF28_DLR_v01_global.vrt')

Then, build a window & read for Bristol, England:

bristol_window = handler.window(-2.75,51.25,-2.25,51.75)
bristol_transform = handler.window_transform(bristol_window)
bristol = handler.read(window=bristol_window)

I do some sklearn magic to derive labels_, which gives an additional classification for each pixel

labels_ = classifier.fit(bristol)

and was intending to map that alongside the bristol boundary, using the transforms:

to_longlat = lambda row,col: (col,row) * bristol_transform * rio.Affine(.5,.5)

Applying this over my window gives me a slightly-incorrect adjustment:
rasterio_transform_issue
but, switching to

# force affine combination before hitting the row-col
to_longlat = lambda row,col: (col,row) * (bristol_transform * rio.Affine(.5,.5))

gives what I'm expecting.
rasterio_transform_ordered

Is this intentional? I usually expect * to be undirected and @ to be directed, but maybe I'm just missing what's implied by multiplication here.

Type hints?

👋

I saw mapbox/mercantile#140 and since @sgillies was receptive to the idea of adding type hints, I thought I'd also start a discussion here about adding type hints 🙂 .

Affine is a relatively small and self-contained library, so it could be a good library in the ecosystem to start with. I'd be happy to put up a PR if there's interest.

One possible hiccup that I found in the creation of external type stubs for affine is that mypy complained about violating the Liskov substitution principle. Specifically I wasn't able to type __mul__ to take an Affine object as input and output, because that's an incompatible override of the NamedTuple's __mul__ typing.

1.2 release

Pickling and a bug fix.

  • close issues
  • tag
  • upload dists

to/from World File order

Not sure if this really belongs here but in keeping with the spirit of to_gdal and from_gdal methods, it might be good to provide wrappers for the World File ordering: a, d, b, e, c*, f*

The only complication is that c* and f* refer to the center of the upper left cell so we'd need to pad them by 1/2 a and e.

affine.EPSILON is too big?

I'm getting degenerate affine transformations for images with a very small cell size, like Landsat scenes in WGS84. Is 1-e5 the default because it makes sense for mathematics? I know there is a set_epsilon() function to manage this variable but wanted to get your thoughts about this before I dug deeper.

Here's an example of a Landsat scene going from UTM -> WGS84 for anyone else that might encounter this issue. Affine.determinant and affine.EPSILON are used in Affine.is_degenerate.

cc/ @Yolandeezy

Check the raw data

Captain:BAD kwurster$ rio insp LC80150332015197LGN00_B5.TIF 
Rasterio 0.25.0 Interactive Inspector (Python 2.7.10)
Type "src.meta", "src.read(1)", or "help(src)" for more information.
>>> src.affine
Affine(30.0, 0.0, 220485.0,
       0.0, -30.0, 4426815.0)
>>> ~src.affine
Affine(0.03333333333333333, 0.0, -7349.5,
       0.0, -0.03333333333333333, 147560.5)
>>> exit()

Reproject to WGS84

Captain:BAD kwurster$ rio warp LC80150332015197LGN00_B5.TIF warped.tif --dst-crs EPSG:4326

Check the reprojected data

Captain:BAD kwurster$ rio insp warped.tif 
Rasterio 0.25.0 Interactive Inspector (Python 2.7.10)
Type "src.meta", "src.read(1)", or "help(src)" for more information.
>>> src.affine
Affine(0.0003139959197432608, 0.0, -78.27163078313896,
       0.0, -0.0003139959197432608, 39.99035001263342)
>>> ~src.affine
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python2.7/site-packages/affine/__init__.py", line 423, in __invert__
    "Cannot invert degenerate transform")
TransformNotInvertibleError: Cannot invert degenerate transform
>>> src.affine.determinant
-9.859343761541627e-08
>>> import affine
>>> affine.EPSILON
1e-05

Interoperability with shapely affinity module

Shapely's affinity module expects a tuple with the (a, b, d, e, xoff, yoff) order. I suggest adding a method to the Affine class to improve interoperability with shapely.
Let me know if that's an interesting proposal and I'll send a PR.

Loïc

[Enhancement] Affine to be transparently multipliable with Numpy arrays

I think that most people who are using affine will also be using rasterio and, thus, working with numpy arrays. It would be nice to be able to transparently batch convert numpy arrays of coordinates with just anAffine * aNumpyArray.

As far as I know, it's standard to have lists of coordinates in numpy arrays shaped [..., 2] because it makes operations like "add (2, 1) to all coordinates" a very simple numpy operation (arr + (2, 1)).

This is what I would like to work:

import affine
import numpy as np
arr = np.array([(1, 1), (4, 6), (2, 5), (3, 1)])
aff = affine.Affine(2, 0, 3, 0, 3, 1) # scale up by (2, 3), offset by (3, 1)
print(arr * aff)

There is a work-around: you can transpose the input, and combine back into a numpy array with:

>>> print(np.array(aff * arr.T).T)
[[ 5.  4.]
 [11. 19.]
 [ 7. 16.]
 [ 9.  4.]]

But, I think it would be much nicer if aff * arr was just supported to do this operation automatically.

In board, zoom scroll jumps too fast from 10% 70% 250%

I am using the affine client, I don't know if it is a problem with the client, or Affine itself.

Tried on different computers with different scroll settings, but the result is the same.

Vertical scrolling works at the correct speed.

OS: Windows 11

Outdated code from python 2 / 3 compatibility

I am not sure but I think this is outdated code for python2 and 3 compatibility?

# Define assert_unorderable() depending on the language
# implicit ordering rules. This keeps things consistent
# across major Python versions
try:
3 > ""
except TypeError: # pragma: no cover
# No implicit ordering (newer Python)
def assert_unorderable(a, b):
"""Assert that a and b are unorderable"""
return NotImplemented
else: # pragma: no cover
# Implicit ordering by default (older Python)
# We must raise an exception ourselves
# To prevent nonsensical ordering
def assert_unorderable(a, b):
"""Assert that a and b are unorderable"""
raise TypeError("unorderable types: %s and %s"
% (type(a).__name__, type(b).__name__))

Removal of __rmul__ breaks Rasterio

Rasterio has some poor affine usage in one function, computing position * matrix. I've fixed that, but even though matrix * position is more proper we need to bring __rmul__ back in case others are depending on it.

Degeneracy test should be exact, not approximate

Background: this affine package is used in Rasterio to represent the affine transformation matrix for georeferenced raster data (satellite imagery, etc).

Geospatial raster data rarely has rotation in the matrix (not never, but rarely) so the determinant of the matrix is most often equal to the product of the raster's pixel width and height. When the raster's georeferencing is in units of meters or feet, its transform will be invertible using affine 1.2.0. However, the same class of raster data – Landsat 8 with 30 meter pixels, say – in a geographic projection with units of decimal degrees (30 meters ~= 0.0003 degrees) will have transform determinants on the order of 1e-7. They're perfectly invertible but affine 1.2.0 says no unless you adjust the global EPSILON value.

It's common in Rasterio to be working with one transform that has a determinant on the order of -1000 and another with a determinant on the order of -1e-7 and both of them invertible. Juggling the global EPSILON is error prone. I propose to make the is_degenerate predicate exact and not approximate, replacing EPSILON with 0.

1.3 Release

The almost-degenerate test for whether a transform is invertible isn't working for Rasterio. I propose to make a 1.3.0 release in which the is_degenerate predicate evaluates self.determinant == 0.0. That would be the only change.

Target: mid-April.

/cc @perrygeo

Floating Point Issue and Affine

Not charactizing affine transformation correctly. Sorry. Closed.


Hi Affine team,

Very much enjoy using this library via rasterio.

I am pretty sure this is purely a floating point issue, but wanted to post to get clarity. Works as expected!

In [1]: from affine import Affine

In [2]: t = Affine(0.00041666666666666664, 0.0 , -181.00020833333335, 0.0, -0.0002777777777777778, 51.75013888888889)

In [3]: t * (.5, .5)
Out[3]: (-181.0, 51.75)

In [4]: -181.00020833333335 + 0.00041666666666666664 * .5
Out[4]: -180.0

In [5]: 51.75013888888889 - 0.0002777777777777778 * .5
Out[5]: 51.75

It appears the transform is rounding to 4 digits.

`__mul__` doesn't always return NotImplemented on failure

I'm trying to implement __rmul__ for a custom type that takes Affine object on the left A * g --> g.__rmul__(A), this works fine except when g is iterable containing exactly two elements. Since then A.__mul__(g) goes into code path listed below and fails with ValueError

affine/affine/__init__.py

Lines 512 to 516 in 78c20a0

try:
vx, vy = other
except Exception:
return NotImplemented
return (vx * sa + vy * sb + sc, vx * sd + vy * se + sf)

I think return statement on line 516 above should be moved inside the try block.

Make EPSILON a class property, not global

The module-level global is problematic. As outlined here: rasterio/rasterio#430 (comment)

>>> import affine as affineA
>>> import affine as affineB
>>> affineA.EPSILON
1e-05
>>> affineB.EPSILON
1e-05
>>> instA = affineA.Affine(0.001215, 0.0, -120.9375, 0.0, -0.001215, 38.823)
>>> instA.is_degenerate
True
>>> affineA.set_epsilon(1e-20)
>>> instA.is_degenerate  # doesn't change existing instances
True
>>> instB = affineB.Affine(0.001215, 0.0, -120.9375, 0.0, -0.001215, 38.823)
>>> instB.is_degenerate  # but it does change new instances, even from different module names
False
>>> affineB.EPSILON
1e-20
>>> affineA.EPSILON
1e-20

Three thoughts

  • We should address this by making epsilon a property of the Affine class, not a global.
  • add a Affine.adjust_epsilon_assume_invertable() method as a convenience method to check for invertability and bump up the epsilon for that instance under the assumption that it is invertable. Similarly we could have a flag to turn off is_degenerate checks for an instance.
  • Would there be any harm in defaulting the EPSILON to a very low value like 1e-40?

Affine class not compatible with Pydantic

I wanted to use Pydantic validators for making data types. However, it does not work properly in Affine 2.3.1:

from pydantic import BaseModel
from affine import identity

class Transform(BaseModel):
    transform: Affine

Transform(transform=identity)

This returns the following error:

---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
<ipython-input-45-0e11ed9741f8> in <cell line: 1>()
----> 1 Transform(transform=identity)

~/anaconda3/envs/mapOsirisv2/lib/python3.10/site-packages/pydantic/main.cpython-310-x86_64-linux-gnu.so in pydantic.main.BaseModel.__init__()

ValidationError: 1 validation error for Transform
transform
  Affine.__new__() got an unexpected keyword argument 'g' (type=type_error)

This is similar to issue #79 .

Add support for pickle

Instances of affine.Affine() can be pickled, but they can't be unpickled, which means they can't be used with modules like multiprocessing because elements 7, 8, and 9 are given to __new__ when the object is unpickled.

Here's an example:

from multiprocessing import Pool

import affine


def processor(x):
    assert isinstance(x, affine.Affine)
    return x

a1 = affine.Affine(1, 2, 3, 4, 5, 6)
a2 = affine.Affine(6, 5, 4, 3, 2, 1)
results = Pool(1).map(processor, [a1, a2])
for expected, actual in zip([a1, a2], results):
    assert expected == actual
Process PoolWorker-1:
Traceback (most recent call last):
  File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/multiprocessing/process.py", line 258, in _bootstrap
    self.run()
  File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/multiprocessing/process.py", line 114, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/multiprocessing/pool.py", line 102, in worker
    task = get()
  File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/multiprocessing/queues.py", line 376, in get
    return recv()
  File "/usr/local/lib/python2.7/site-packages/affine/__init__.py", line 152, in __new__
    "Expected 6 coefficients, found %d" % len(members))
TypeError: Expected 6 coefficients, found 9

Rotate by radians ?

In Affine, rotations are always in degrees, but it can sometimes be useful to expose radians.

If you are considering changing the API, would you consider exposing an API that works in radians ?

Add Affine to homebrew ?

I had a quick look and on ubuntu there is python3-affine, and at least on rpm distros there is a package of the same name, so things are looking pretty good for linux outside of pip (which is pretty big for beginners).

On the mac side I couldn't find Affine on homebrew, would it be possible to add a formula for it there ?

https://docs.brew.sh/Adding-Software-to-Homebrew

Read/write world files

As discussed several months ago on rasterio#80, I thought it would be convenient to read/write world files, which use the same 6 coefficients as Affine.

Any ideas for names, prototypes, behaviour? The reader will certainly be another @classmethod that returns an Affine object. Initially, I was thinking from_worldfile(fp), but it could also be load_worldfile(fp) and/or loads_worldfile(s) for reading. Similarly, dump_worldfile(fp) and/or dumps_worldfile() for writing.

1.0 release

TODO:

  • Tag
  • Upload to PyPI

@caseman this Affine package is derived from your Planar package. Affine is a beautiful class and super useful for my GIS raster data package, Rasterio. Is the attribution in README.md and affine/init.py to your satisfication?

Docs (readthedocs) ?

Planar had some nice HTML docs
https://pythonhosted.org/planar/transforms.html

I'm not sure how the above docs are generated, if they are something standard it may be worth moving docs to readthedocs.

It would be good to get these for Affine, it may be a matter of contacting Casey, I know a number of years ago he was pretty keen for someone to pick up Planar, so don't imagine this would be a problem.

Warn or forbit left multiplication

Because of how __rmul__ is implemented, left multiplication can give surprising results:

>>> Affine.translation(1, 1) * Affine.scale(2, 3) * (1, 1)
(3.0, 4.0)
>>> (Affine.translation(1, 1) * Affine.scale(2, 3)) * (1, 1)
(3.0, 4.0)
>>> (1, 1) * Affine.translation(1, 1) * Affine.scale(2, 3)
(4.0, 6.0)
>>> (1, 1) * (Affine.translation(1, 1) * Affine.scale(2, 3))
(3.0, 4.0)

I think you already anticipated this:

https://github.com/sgillies/affine/blob/2.1.0/affine/__init__.py#L470-L472

However, Shapely shapely.affinity.affine_transform performs left multiplication and uses the GDAL order, so trying to mix these two can be a pain to get right. I think this should be at least document, but perhaps better raise a warning or even an exception.

Add `from_bounds` and `from_origin` class methods

The rasterio.transform module has several methods for generating an instance of affine.Affine. I don't think these methods are specific to rasterio, and given that they just call Affine class methods, I think it would be useful to define these here.

If you think this is a good idea I'm happy to write up a PR!

Parameter description

Understand there are three additional parameters, perhaps a brief note (I may have missed) would be of use.

• a = width of a pixel
• b = row rotation (typically zero)
• c = x-coordinate of the upper-left corner of the upper-left pixel
• d = column rotation (typically zero)
• e = height of a pixel (typically negative)
• f = y-coordinate of the of the upper-left corner of the upper-left pixel

Affine argument types - incorrect?

versions

$ pip freeze | grep rasterio
rasterio==1.0.25
$ pip freeze | grep affine
affine==2.2.2

typing issue

PyCharm warns that Affine expects a List[float] but it doesn't work that way, it works with individual float arguments (see screenshot attached). Is the typing markup out of sync with the function arguments?

Screen Shot 2019-08-28 at 4 09 53 PM

Don't understand how PyCharm generates that warning from the class definition, maybe something related to this doc-string?

If an actual List[float] is provided, the TypeError is triggered because len(members) == 1

Generate WHLs for Windows

It would be good if WHLs could be generated for Windows, since it is a lot less likely people have a compiler there.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.