Coder Social home page Coder Social logo

bcollazo / catanatron Goto Github PK

View Code? Open in Web Editor NEW
276.0 11.0 64.0 45.52 MB

Settlers of Catan Bot Simulator and Strong AI Player

Home Page: https://catanatron.com

License: GNU General Public License v3.0

Python 82.77% HTML 0.43% CSS 0.06% JavaScript 13.15% SCSS 2.20% Dockerfile 0.06% Jupyter Notebook 1.12% Makefile 0.20% Procfile 0.01%
machine-learning python catan ai bot

catanatron's Introduction

Catanatron

Coverage Status Documentation Status Join the chat at https://gitter.im/bcollazo-catanatron/community Open In Colab

Settlers of Catan Bot and Bot Simulator. Test out bot strategies at scale (thousands of games per minutes). The goal of this project is to find the strongest Settlers of Catan bot possible.

See the motivation of the project here: 5 Ways NOT to Build a Catan AI.

Installation

Clone this repository and install dependencies. This will include the Catanatron bot implementation and the catanatron-play simulator.

git clone [email protected]:bcollazo/catanatron.git
cd catanatron/

Create a virtual environment with Python3.8 or higher. Then:

pip install -r all-requirements.txt

Usage

Run simulations and generate datasets via the CLI:

catanatron-play --players=R,R,R,W --num=100

See more information with catanatron-play --help.

Try Your Own Bots

Implement your own bots by creating a file (e.g. myplayers.py) with some Player implementations:

from catanatron import Player
from catanatron_experimental.cli.cli_players import register_player

@register_player("FOO")
class FooPlayer(Player):
  def decide(self, game, playable_actions):
    """Should return one of the playable_actions.

    Args:
        game (Game): complete game state. read-only.
        playable_actions (Iterable[Action]): options to choose from
    Return:
        action (Action): Chosen element of playable_actions
    """
    # ===== YOUR CODE HERE =====
    # As an example we simply return the first action:
    return playable_actions[0]
    # ===== END YOUR CODE =====

Run it by passing the source code to catanatron-play:

catanatron-play --code=myplayers.py --players=R,R,R,FOO --num=10

How to Make Catanatron Stronger?

The best bot right now is Alpha Beta Search with a hand-crafted value function. One of the most promising ways of improving Catanatron is to have your custom player inhert from (AlphaBetaPlayer) and set a better set of weights for the value function. You can also edit the value function and come up with your own innovative features!

For more sophisticated approaches, see example player implementations in catanatron_core/catanatron/players

If you find a bot that consistently beats the best bot right now, please submit a Pull Request! :)

Advanced Usage

Inspecting Games (Browser UI)

We provide a docker-compose.yml with everything needed to watch games (useful for debugging). It contains all the web-server infrastructure needed to render a game in a browser.

To use, ensure you have Docker Compose installed, and run (from this repo's root):

docker-compose up

You can now use the --db flag to make the catanatron-play simulator save the game in the database for inspection via the web server.

catanatron-play --players=W,W,W,W --db --num=1

NOTE: A great contribution would be to make the Web UI allow to step forwards and backwards in a game to inspect it (ala chess.com).

Accumulators

The Accumulator class allows you to hook into important events during simulations.

For example, write a file like mycode.py and have:

from catanatron import ActionType
from catanatron_experimental import SimulationAccumulator, register_accumulator

@register_accumulator
class PortTradeCounter(SimulationAccumulator):
  def before_all(self):
    self.num_trades = 0

  def step(self, game_before_action, action):
    if action.action_type == ActionType.MARITIME_TRADE:
      self.num_trades += 1

  def after_all(self):
    print(f'There were {self.num_trades} port trades!')

Then catanatron-play --code=mycode.py will count the number of trades in all simulations.

As a Package / Library

You can also use catanatron package directly which provides a core implementation of the Settlers of Catan game logic.

from catanatron import Game, RandomPlayer, Color

# Play a simple 4v4 game
players = [
    RandomPlayer(Color.RED),
    RandomPlayer(Color.BLUE),
    RandomPlayer(Color.WHITE),
    RandomPlayer(Color.ORANGE),
]
game = Game(players)
print(game.play())  # returns winning color

You can use the open_link helper function to open up the game (useful for debugging):

from catanatron_server.utils import open_link
open_link(game)  # opens game in browser

Architecture

The code is divided in the following 5 components (folders):

  • catanatron: A pure python implementation of the game logic. Uses networkx for fast graph operations. Is pip-installable (see setup.py) and can be used as a Python package. See the documentation for the package here: https://catanatron.readthedocs.io/.

  • catanatron_server: Contains a Flask web server in order to serve game states from a database to a Web UI. The idea of using a database, is to ease watching games played in a different process. It defaults to using an ephemeral in-memory sqlite database. Also pip-installable (not publised in PyPi however).

  • catanatron_gym: OpenAI Gym interface to Catan. Includes a 1v1 environment against a Random Bot and a vector-friendly representations of states and actions. This can be pip-installed independently with pip install catanatron_gym, for more information see catanatron_gym/README.md.

  • catantron_experimental: A collection of unorganized scripts with contain many failed attempts at finding the best possible bot. Its ok to break these scripts. Its pip-installable. Exposes a catanatron-play command-line script that can be used to play games in bulk, create machine learning datasets of games, and more!

  • ui: A React web UI to render games. This is helpful for debugging the core implementation. We decided to use the browser as a randering engine (as opposed to the terminal or a desktop GUI) because of HTML/CSS's ubiquitousness and the ability to use modern animation libraries in the future (https://www.framer.com/motion/ or https://www.react-spring.io/).

AI Bots Leaderboard

Catanatron will always be the best bot in this leaderboard.

The best bot right now is AlphaBetaPlayer with n = 2. Here a list of bots strength. Experiments done by running 1000 (when possible) 1v1 games against previous in list.

Player % of wins in 1v1 games num games used for result
AlphaBeta(n=2) 80% vs ValueFunction 25
ValueFunction 90% vs GreedyPlayouts(n=25) 25
GreedyPlayouts(n=25) 100% vs MCTS(n=100) 25
MCTS(n=100) 60% vs WeightedRandom 15
WeightedRandom 53% vs WeightedRandom 1000
VictoryPoint 60% vs Random 1000
Random - -

Developing for Catanatron

To develop for Catanatron core logic you can use the following test suite:

coverage run --source=catanatron -m pytest tests/ && coverage report

Or you can run the suite in watch-mode with:

ptw --ignore=tests/integration_tests/ --nobeep

Machine Learning

Generate JSON files with complete information about games and decisions by running:

catanatron-play --num=100 --output=my-data-path/ --json

Similarly (with Tensorflow installed) you can generate several GZIP CSVs of a basic set of features:

catanatron-play --num=100 --output=my-data-path/ --csv

You can then use this data to build a machine learning model, and then implement a Player subclass that implements the corresponding "predict" step of your model. There are some attempts of these type of players in reinforcement.py.

Appendix

Running Components Individually

As an alternative to running the project with Docker, you can run the following 3 components: a React UI, a Flask Web Server, and a PostgreSQL database in three separate Terminal tabs.

React UI

cd ui/
npm install
npm start

This can also be run via Docker independetly like (after building):

docker build -t bcollazo/catanatron-react-ui:latest ui/
docker run -it -p 3000:3000 bcollazo/catanatron-react-ui

Flask Web Server

Ensure you are inside a virtual environment with all dependencies installed and use flask run.

python3.8 -m venv venv
source ./venv/bin/activate
pip install -r requirements.txt

cd catanatron_server/catanatron_server
flask run

This can also be run via Docker independetly like (after building):

docker build -t bcollazo/catanatron-server:latest . -f Dockerfile.web
docker run -it -p 5000:5000 bcollazo/catanatron-server

PostgreSQL Database

Make sure you have docker-compose installed (https://docs.docker.com/compose/install/).

docker-compose up

Or run any other database deployment (locally or in the cloud).

Other Useful Commands

TensorBoard

For watching training progress, use keras.callbacks.TensorBoard and open TensorBoard:

tensorboard --logdir logs

Docker GPU TensorFlow

docker run -it tensorflow/tensorflow:latest-gpu-jupyter bash
docker run -it --rm -v $(realpath ./notebooks):/tf/notebooks -p 8888:8888 tensorflow/tensorflow:latest-gpu-jupyter

Testing Performance

python -m cProfile -o profile.pstats catanatron_experimental/catanatron_experimental/play.py --num=5
snakeviz profile.pstats
pytest --benchmark-compare=0001 --benchmark-compare-fail=mean:10% --benchmark-columns=min,max,mean,stddev

Head Large Datasets with Pandas

In [1]: import pandas as pd
In [2]: x = pd.read_csv("data/mcts-playouts-labeling-2/labels.csv.gzip", compression="gzip", iterator=True)
In [3]: x.get_chunk(10)

Publishing to PyPi

catanatron Package

make build PACKAGE=catanatron_core
make upload PACKAGE=catanatron_core
make upload-production PACKAGE=catanatron_core

catanatron_gym Package

make build PACKAGE=catanatron_gym
make upload PACKAGE=catanatron_gym
make upload-production PACKAGE=catanatron_gym

Building Docs

sphinx-quickstart docs
sphinx-apidoc -o docs/source catanatron_core
sphinx-build -b html docs/source/ docs/build/html

Contributing

I am new to Open Source Development, so open to suggestions on this section. The best contributions would be to make the core bot stronger.

Other than that here is also a list of ideas:

  • Improve catanatron package running time performance.

    • Continue refactoring the State to be more and more like a primitive dict or array. (Copies are much faster if State is just a native python object).
    • Move RESOURCE to be ints. Python enums turned out to be slow for hashing and using.
    • Move .actions to a Game concept. (to avoid copying when copying State)
    • Remove .current_prompt. It seems its redundant with (is_moving_knight, etc...) and not needed.
  • Improve AlphaBetaPlayer:

    • Explore and improve prunning
    • Use Bayesian Methods or SPSA to tune weights and find better ones.
  • Experiment ideas:

    • DQN Render Method. Use models/mbs=64__1619973412.model. Try to understand it.
    • DQN Two Layer Algo. With Simple Action Space.
    • Simple Alpha Go
    • Try Tensorforce with simple action space.
    • Try simple flat CSV approach but with AlphaBeta-generated games.
    • Visualize tree with graphviz. With colors per maximizing/minimizing.
    • Create simple entry-point notebook for this project. Runnable via Paperspace. (might be hard because catanatron requires Python 3.8 and I haven't seen a GPU-enabled tensorflow+jupyter+pyhon3.8 Docker Image out there).
  • Bugs:

    • Shouldn't be able to use dev card just bought.
  • Features:

    • Continue implementing actions from the UI (not all implemented).
    • Chess.com-like UI for watching game replays (with Play/Pause and Back/Forward).
    • A terminal UI? (for ease of debugging)

catanatron's People

Contributors

alastair-l avatar bcollazo avatar fyordan avatar gitter-badger avatar hassanjbara avatar pachewise avatar snyk-bot avatar tonypr avatar zarns 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  avatar  avatar  avatar

catanatron's Issues

[React] Implement MOVE_ROBBER action

Right now the UI skips the MOVE_ROBBER action by deciding on behalf of the human player. Allow for human players to select the tile where they want to move the robber.

Write tests to ensure stealing road works

I am not completely sure that if a player ties for longest road, the code correctly doesn't award the 2 victory points, but only does so when the player surpasses longest road by at least 1.

Improve catanatron-play load time

Right now, even using the --help flag takes a long time. I am assuming it's because we are eagerly importing libraries (for example, numpy, tensorflow) which may not be needed until the actual code is being executed.

This may be a symptom of a broader need for import / loading hygiene.

time catanatron-play --help gave 20s on my MacBook Pro:

image

image

[Python] Make MARITIME_TRADE action's value a 10-integer list

Right now trade actions look like:

Action(Color.BLUE, ActionType.MARITIME_TRADE, ("WOOD", "WOOD", None, None, "SHEEP")) # 2 woods for a sheep
Action(Color.BLUE, ActionType.MARITIME_TRADE, ("BRICK", "BRICK", "BRICK", "BRICK", "WHEAT")) # 4 bricks for a wheat

Seems better to encode these as a union of two "freqdecks". This will make it more vector-friendl, potentially faster, and more interoperable with the rest of the code.

Action(Color.BLUE, ActionType.MARITIME_TRADE, (2,0,0,0,0,0,0,1,0,0)) # 2 woods for a sheep
Action(Color.BLUE, ActionType.MARITIME_TRADE, (0,4,0,0,0,0,0,0,1,0)) # 4 bricks for a wheat

Simulator CLI seems to ignore first player code in list and also fails with 5 or longer (thus preventing a 4 player workaround)

If I try to run catanatron-play -players=VP,AB --num=2 -o sims --csv I get the following errors, which I believe are due to the cli only picking up one player code:

(venv) paul@DESKTOP-65EVJAG:~/environments/catanatron$ catanatron-play -players=VP,AB --num=2 -o sims --csv
Playing 2 games...                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% -:--:--
AlphaBetaPlayer:BLUE(depth=2,value_fn=base_fn,prunning=False) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0%
Traceback (most recent call last):
  File "/home/paul/environments/catanatron/venv/bin/catanatron-play", line 11, in <module>
    load_entry_point('catanatron-experimental', 'console_scripts', 'catanatron-play')()
  File "/home/paul/environments/catanatron/venv/lib/python3.8/site-packages/click/core.py", line 1128, in __call__
    return self.main(*args, **kwargs)
  File "/home/paul/environments/catanatron/venv/lib/python3.8/site-packages/click/core.py", line 1053, in main
    rv = self.invoke(ctx)
  File "/home/paul/environments/catanatron/venv/lib/python3.8/site-packages/click/core.py", line 1395, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/paul/environments/catanatron/venv/lib/python3.8/site-packages/click/core.py", line 754, in invoke
    return __callback(*args, **kwargs)
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/play.py", line 184, in simulate
    play_batch(
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/play.py", line 314, in play_batch
    for i, game in enumerate(
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/play.py", line 245, in play_batch_core
    game.play(accumulators)
  File "/home/paul/environments/catanatron/catanatron_core/catanatron/game.py", line 103, in play
    self.play_tick(decide_fn=decide_fn, accumulators=accumulators)
  File "/home/paul/environments/catanatron/catanatron_core/catanatron/game.py", line 124, in play_tick
    else player.decide(self, actions)
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/minimax.py", line 70, in decide
    result = self.alphabeta(
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/minimax.py", line 121, in alphabeta
    result = self.alphabeta(
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/minimax.py", line 121, in alphabeta
    result = self.alphabeta(
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/minimax.py", line 100, in alphabeta
    value = value_fn(game, self.color)
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/value.py", line 63, in fn
    enemy_production = value_production(enemy_production_sample, "P1", False)
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/value.py", line 133, in value_production
    prod_sum = sum([sample[f] for f in features])
  File "/home/paul/environments/catanatron/catanatron_experimental/catanatron_experimental/machine_learning/players/value.py", line 133, in <listcomp>
    prod_sum = sum([sample[f] for f in features])
KeyError: 'EFFECTIVE_P1_WHEAT_PRODUCTION'

If I try to run with 4 players, I only get 3 (and with 3, I get 2).

If I try to run with 5 players, I also get an out of bounds error which I assume is expected.

Implement Card Counting

Keeping track of your enemies' cards is usually helpful. Implementing this could:

Would be cool to have this "card counting capability" be parametrized by a number K that represents how many cards from its enemy can a player keep "in its mind". I myself as a player, can probably just keep the last 2 - 3 cards of my enemies hands 😅 .

There is a question whether to implement this as part of the Game, or as a capability in the Players. Intuitively, sounds like it should be the Player (since some may have the capabilities, but others not); but I am not sure how the API would like. Do we have to now feed every "tick" to every player(?) Do we make it so that the player has a pointer to the log of ticks/actions and knows how many it has "consumed", so that the next time it is its turn, he "updates" its card counting distribution/believe? In that later approach, we would have to make sure reading the log doesn't give the player more information than what he should have.

This sounds like could be developed as a fairly independent module (say counter = CardCounter(k=5) and counter.update(...)), that players may or may not use in their API.

Implement Player to Player trading

Still unsure how implementation should work, but it seems the tick_queue approach would allow for the trading conversation to happen (e.g. P0 offers X, P1 decides against, P1 counter-offers, P0 counter-offers). Important for Random Bots not to get stuck here.

pips/dots for numbers in UI

Would be helpful to have the dots for the numbers in the UI so you can easily tell which are the higher probability rolls

Gym seems to be broken?

Having issues with unpacking certain things when I try to just run the default example on the readme. I think it has to do with the change to gymnasium as well as the addition of "truncated". Would appreciate if you could look into it, thanks!

Not able to play VP on devcards

    # Dev Card Plays
    PLAY_KNIGHT_CARD = "PLAY_KNIGHT_CARD"  # value is None
    PLAY_YEAR_OF_PLENTY = "PLAY_YEAR_OF_PLENTY"  # value is (Resource, Resource)
    PLAY_MONOPOLY = "PLAY_MONOPOLY"  # value is Resource
    PLAY_ROAD_BUILDING = "PLAY_ROAD_BUILDING"  # value is None

Here at the enums part of catanatron_core I couldn't see PLAY_VICTORY_POINT or such a naming. Why there isn't one?

Fix Overview.ipynb

When running it on Collab, we get:

---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
[<ipython-input-3-e16933b01ac9>](https://localhost:8080/#) in <module>
      1 from catanatron.json import GameEncoder
      2 from catanatron_gym.features import create_sample_vector, create_sample
----> 3 from catanatron_experimental.machine_learning.board_tensor_features import (
      4     create_board_tensor,
      5 )

ModuleNotFoundError: No module named 'catanatron_experimental.machine_learning.board_tensor_features'

---------------------------------------------------------------------------
NOTE: If your import is failing due to a missing package, you can
manually install dependencies using either !pip or !apt.

To view examples of installing some common dependencies, click the
"Open Examples" button below.
---------------------------------------------------------------------------

Create /mcts-score endpoint

Create an endpoint with which we can get the MCTS Score out of a given position.

This could be used to show in the UI a stacked bar with the probability of each player winning. We can improve the evaluator in the future.

For now, we can have it just simulate 100 Random Games from the given state and return the % of games each player won.

The endpoint probably looks like GET /games/:uid/states/:uid/mcts-score

[Python] Remove nxgraph dependency

It seems using a pure-python dictionary-based data structure for graph operations makes catanatron simulate games faster than using the nxgraph library. nxgraph is mostly used today just to consult neighbors for a given node, and so building a lookup table at game initialization (or even at simulation initialization) could make catanatron faster.

Implement Card-Counting / Inference Algorithms

It would be nice to have algorithms so that players can keep track of enemy cards. This should make bots stronger.

It should be parameterized say by "CARDS_IN_MIND" so that we can play with how "strong" the inference is and how does it affect game play. I myself can't memorize too many cards on enemy hands. :)

[React][UI] Can't cancel road building animation if started

If you select "Buy > Road", the animation asking which road will start, but there is no way to "regret" this decision. It'd be nice if clicking on the "Buy > Road" button again to cancel the action.

Screenshot 2024-07-04 at 7 39 14 AM

For now, clicking "End (Turn)" cancels it.

Find a better way to for-see moves

Doing a copy.deepcopy of the complete game state is expensive. If we could find a way to either copy game state faster, or somehow use a fast immutable data structure for game state; or have game-state be c-powered numpy arrays, it might allow us to compute "later-in-time" calculations (explore the decision tree) faster.

Allow to "replay games" in UI

Create functionality to allow replaying a game from start to finish ala "chess.com". This will be super helpful for debugging bot decision logic and extracting specific middle-of-the-game game states for further analysis.

Refactor code to avoid eager dependencies

Right now it seems tensorflow, numpy, and pandas are only needed for CsvDataAccumulator which seems to be a Machine Learning inclined feature. It would be nice if the catanatron could come with the simulator without having to include heavy dependencies like these (tensorflow, numpy, and pandas). Actually, maybe even without including rich or click.

This issue is to study and think about how could we structure the codebase so that people could use the simulator by just pip install catanatron and something like:

from catanatron import RandomPlayer, Color, play_batch

# Play a simple 4v4 game
players = [
    RandomPlayer(Color.RED),
    RandomPlayer(Color.BLUE),
    RandomPlayer(Color.WHITE),
    RandomPlayer(Color.ORANGE),
]
results = play_batch(100, players)  # simulates 100 games

or so.

Maybe have a catanatron_cli package that uses this core play_batch and includes the click and rich libraries to do its thing? Maybe another catanatron_ai that includes the ML-based features like the CsvDataAccumulator and the AI-based players that depend on tensorflow, numpy, and pandas?

This would help with slow startup time #208 and would make it easier to adopt in M1 machines where tensorflow is a little more involved to install. Open to thoughts.

docker-compose images are outdated

Running the docker-compose file to start the react server fails in the react-ui container due to this error: The engine "node" is incompatible with this module. Expected version ">=16.0.0 <17.0.0". Got "15.5.0". I looked into the bcollazo/catanatron-react-ui:latest image and it seems like it indeed uses a wrong node version. Furthermore, the server image uses a wrong flask server file (catanatron_server/server instead of catanatron_server/catanatron_server).

Suggested Solution

Either update the images or simply comment out the image option under the react-ui and server containers in docker-compose, forcing docker to build the images from the local dockerfiles instead.

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.