Giter VIP home page Giter VIP logo

web3studio-soy's Introduction



ENS+IPFS ❤ DevOps - Static Websites on the Distributed Web

UsagePackagesAPIContributingLicense

Soy is a collection of smart contracts and tools to enable you to build your site on the distributed web. By virtue of using ENS and IPFS your content will be quickly accessible all over the world without having to set up or manage any infrastructure.

Already have an ENS resolver? Add .soy to the end to see it in your browser! Check out web3studio.eth.soy


Usage

Install

# Yarn
$ yarn add --dev soy-core

# NPM
$ npm install --save-dev soy-core

Configure

Create a new soy instance and give it any Web3 provider.

const Soy = require('soy-core');
const HDWalletProvider = require('truffle-hdwallet-provider');

const mnemonic = process.env.WALLET_MNEMONIC;
const infuraApiKey = process.env.INFURA_API_KEY;
const infuraNetwork = process.env.INFURA_NETWORK;
const provider = new HDWalletProvider(
  mnemonic,
  `https://${infuraNetwork}.infura.io/v3/${infuraApiKey}`
);

const soy = new Soy(provider);

Scripting

Scripting with Soy usually looks something like this. This is an example of creating a new Soy instance and using it to register a domain and publish the contenthash for the site.

const Soy = require('soy-core');
const Web3 = require('web3');
const HDWalletProvider = require('truffle-hdwallet-provider');

// Change these paremeters or pass them in as env variables
const mnemonic = process.env.WALLET_MNEMONIC;
const infuraApiKey = process.env.INFURA_API_KEY;
const infuraNetwork = process.env.INFURA_NETWORK || 'rinkeby';
const contentHash = '/ipfs/QmVyYoFQ8KDLMUWhzxTn24js9g5BiC6QX3ZswfQ56T7A5T';
const domain = 'soyexample.test';

var provider = new HDWalletProvider(
  mnemonic,
  `https://${infuraNetwork}.infura.io/v3/${infuraApiKey}`
);

(async () => {
  const web3 = new Web3(provider);
  const accounts = await web3.eth.getAccounts();
  const owner = accounts[0];

  const soy = new Soy(provider, { from: owner });

  const resolver = await soy.registerDomain(domain);
  const revision = await resolver.publishRevision(contentHash);

  console.log(`Revision ${revision} published by Soy!`);
})().catch(console.log);

View Your Beautiful Site

Once you have ENS set up to point to an ipfs hash, simply add .soy to the ENS domain in your browser. For example, web3studio.eth becomes web3studio.eth.soy.

Packages

Soy consists of a bunch of tools that make hosting distributed web sites easy. They are:

Contracts contains the source of the solidity contracts and a low level interface for interactions via truffle-contract.

For more information, see soy-contracts's main page.

The gateway is the source code behind eth.soy. It's a shim to enable browsers to support distributed file systems over ENS until browsers can handle this natively.

For more information, see soy-gateway's main page.

The core project contains a friendly js interface to interacting with the deployed contracts of soy-contracts enabling you to get your content out there with ease.

API

Classes

Ens

Soy's ENS resolver which caches all results per domain's TTL set by it's resolver.

Resolver

A nod specific resolver

Soy

Soy is the best interface for Soy's smart contracts. It provides an easily scriptable interface for any deployment pattern.

Ens

Soy's ENS resolver which caches all results per domain's TTL set by it's resolver.

Kind: global class

new Ens(provider, [registryAddress])

Constructor

Param Type Description
provider Object A web3@1 provider, defaults to localhost
[registryAddress] string An optional registry address for bespoke networks

Example (Get the `contenthash` for a domain)

const siteHash = soy.ens.getContentHash('web3studio.eth');

ens.resolver(domain) ⇒ Promise.<SoyPublicResolver>

Gets a resolver contract instance for a registered ENS domain

Kind: instance method of Ens
Returns: Promise.<SoyPublicResolver> - Resolver for a domain

Param Type Description
domain string ENS domain (eg: web3studio.eth)

ens.getContentHash(domain) ⇒ Promise.<string>

Resolves the contenthash for an ENS domain

Kind: instance method of Ens
Returns: Promise.<string> - The contenthash for the ENS domain

Param Type Description
domain string ENS domain (eg: web3studio.eth)

Resolver

A nod specific resolver

Kind: global class

new Resolver(domain, resolver)

Create a unique contract instance with common params filled in. Wraps all methods of SoyPublicResolver and by extension the base PublicResolver without the need to specify a namehashed domain and tedious unit conversions.

truffle-contract is used to generate the interface. For more detailed explanations, see their docs

Param Type Description
domain string ens domain
resolver SoyPublicResolver A resolver contract

resolver.publishRevision(contentHash, [alias], [txOps]) ⇒ Promise.<number>

Publishes the content hash as a revision

Kind: instance method of Resolver
Returns: Promise.<number> - The revision number

Param Type Description
contentHash string Content hash to publish for your site
[alias] string alias to set for this hash
[txOps] Object web3 transactions options object

resolver.contenthash() ⇒ Promise.<string>

Get the current contenthash

Kind: instance method of Resolver
Returns: Promise.<string> - current resolver content hash

Soy

Soy is the best interface for Soy's smart contracts. It provides an easily scriptable interface for any deployment pattern.

Kind: global class
Properties

Name Type Description
ens ENS ENS resolver utility
web3 Web3 web3.js instance
ipfs IPFS ipfs-http-client instance

new Soy(provider, [options])

Create a new soy instance

Param Type Description
provider Web3.Provider A Web3 provider instance
[options] Object Soy instance options
[options.registryAddress] string An address for a deployed ENS registry
[options.resolverAddress] string An address for a deploy SoyPublicResolver
[...options.txOps] Object Default transaction arguments passed to web3

soy.uploadToIPFSAndPublish(path, domain, [options]) ⇒ Promise.<{hash: string, rev: number}>

Upload the contents of a directory to ipfs and publishes the root folder's hash as a revision

Kind: instance method of Soy
Returns: Promise.<{hash: string, rev: number}> - - The hash published and it's revision number

Param Type Description
path string Path to the directory
domain string ENS domain to publish a revision
[options] Object IPFS options

soy.resolver(domain) ⇒ Promise.<Resolver>

With a registered domain, get a resolver instance for a specific node

Kind: instance method of Soy
Returns: Promise.<Resolver> - - A resolver instance

Param Type Description
domain string The domain for the node

Example (Publish a revision of your site)

const resolver = await soy.resolver('example.madewith.eth');

await resolver.publishRevision(
  '/ipfs/QmVyYoFQ8KDLMUWhzxTn24js9g5BiC6QX3ZswfQ56T7A5T'
);

soy.registerDomain(domain) ⇒ Promise.<Resolver>

Registers a new domain and sets it's resolver to Soy's PublicResolver contract. This will only need to be done once per (sub)domain

If you haven't done so yet, you will need to purchase a domain. We recommend using My Ether Wallet. Domain auctions will last a week.

Kind: instance method of Soy
Returns: Promise.<Resolver> - a resolver instance

Param Type Description
domain string a new ENS domain to register

Example (Register an ENS Domain with Soy)

const resolver = await soy.registerDomain('example.madewith.eth');

Contributing

Please read through our contributing guidelines. Included are directions for coding standards, and notes on development.

License

Apache 2.0

web3studio-soy's People

Contributors

barlock 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

Watchers

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

web3studio-soy's Issues

Deploying soy powered website

Overview

Taking a simple static generated website (using gatsby) and deploy it in soy-fashion into my own AWS account as if I was an external developer trying out soy on my own.

Acceptance criteria

Site is deployed and accessible via public URL

Create soy devkit

Overview

English description of goal in a sentence or two, usually in the form of a user story.

Reference

Questions

  • List any questions?
  • That you are unsure of the answer?

Assumptions

  • List any assumptions that
  • You are making in terms of timeframe,
  • Output, or general context setting

Acceptance

  • List of what needs to be true
  • to consider this done
  • done done

Tasks

  • A list of tasks that need
  • to be done to call this issue done

Run security scan on soy website

Overview

As a user using a soy website, I want to be ensured that I'm as protected as I can be, so that my information isn't stolen by bad actors

Reference

  • #52 - Discovered phishing vector

Questions

  • List any questions?
  • That you are unsure of the answer?

Assumptions

  • List any assumptions that
  • You are making in terms of timeframe,
  • Output, or general context setting

Acceptance

  • List of what needs to be true
  • to consider this done
  • done done

Tasks

  • A list of tasks that need
  • to be done to call this issue done

Cloudfront uses failover origins

Overview

As a user, I want to have maximum site uptime, so that I don't get frustrated having the site down.

Reference

Assumptions

  • A Cloudfront origin group (with some work?) will rotate through origins if one fails
  • All ipfs gateways act the same so using one vs the other doesn't matter too much

Questions

  • What other benefits can we get from origin groups? Geo distribution?

Acceptance

  • If one gateway goes down, move to the next one in the list
  • Gateway order:
    1. Infura (ipfs.infura.io)
    2. IPFS.io (gateway.ipfs.io)
    3. Cloudflare (cloudflare-ipfs.com)
    4. Secondary IPFS? (ipfs.io)

Tasks

Write Readme and API documentation for packages

Overview

As a developer-user of soy, I want to understand the higher level purpose and how to use the project, as well as how to use the underlying projects.

Assumptions

  • Overview of all projects should be in a top level readme
  • API documentation should be generated from jsdoc
  • jsdoc should be generated as commit hook

Acceptance

  • Developers have the tools they need to understand how to use a project
  • Developers know the background of and how to use Soy as a whole

Tasks

  • Write top level documents
  • Generate underlying api documents

Getting Started experience

Overview

As a developer-user, I want to be able to quickly spin up a simple site using Soy so that they can see it's simplicity and usefulness.

Reference

Questions

  • How can we streamline ENS registration?
  • How can we streamline eth for app-payments?

Assumptions

  • we have madewith.eth to hand out subdomains of

Acceptance

  • Users can put an existing static site on soy in under 5 min.

Tasks

  • Understand where the bottlenecks of first time set up are
  • Patch obvious holes that can be fixed
  • Plan any upcoming work
  • ???
  • Profit

Plan the "production" deployment configuration for Soy

Overview

Propose a plan to configure aws accounts, users, roles, such that we can feel comfortable using this setup to deploy "production" versions of our projects.

Reference

"The Plan" lives as a living post in the first #41 (comment)

Questions

  • Do we need two accounts or is IAM good enough?

Assumptions

  • Developer accounts should not be allowed to change production assets
  • Developers should be able to change production assets in an emergency
  • Travis should be able to change production
  • Some kind of integration test suite should exist or be possible
  • Developer accounts should be able to see production logs
  • uptime monitoring should be in place.
    • Downtime alerts sent via slack

Acceptance

  • Documented plan for soy in production

Tasks

  • A list of tasks that need
  • to be done to call this issue done

example web3studio.eth.soy not working

opening the website in Chrome: https://web3studio.eth.soy/
informs of the expired https,
and the it appears a link named web3studio, with content:

ipfs resolve -r /ipfs/QmSC89fFVFFSVC46YmkASGhiwWKhe9MjbmXxceMegeL5zA/ipfs/QmSC89fFVFFSVC46YmkASGhiwWKhe9MjbmXxceMegeL5zA/web3studio: no link named "ipfs" under QmSC89fFVFFSVC46YmkASGhiwWKhe9MjbmXxceMegeL5zA

I guess this is not the indented content of https://web3studio.eth.soy/

is this project up-to-date?
can it be used to deploy a website?

Build lambda gateway

Overview

As a user I want to access a fast website with a nice domain

Reference

Assumptions

  • Koa server deployed via AWS Lambda
  • Use SAM for deployments

Acceptance

  • Publicly accessible gateway
  • Maps via architecture in #1

Tasks

  • Build and test server function
  • Stand up prod instance

v1 Architecture

Overview

Hyperlane's goal is to allow for user friendly static-hosted websites to be served via a distributed storage provider (IPFS initially). While providing friendly domain names for users, it also gives developers flexibility to perform expected DevOps dances like blue/green deploys, dark launches, timed releases, split testing, and slow rollouts to name a few.

Hyperlane:

  • is a collection of ens smart contract resolvers
  • is a handle cli to make updates easy
  • is DevOps and a-b, split testing friendly
  • has user friendly domain names
  • is distributed, resilient, never going down
  • is a static web server (maybe)
  • is a gateway to ipfs (maybe)
  • allows for custom domains
  • uses TLS (https)

How it works

ENS Resolvers

All of the "magic" happens in these bespoke resolvers. ENS is similar to DNS in that you have nested domains *.*.eth where every nested level is it's own smart contract. Smart contracts are programmable! See where this is going?

The hyperlane ens resolvers allows developers to choose which revision of their site is sent to users with smart contracts. For the most part, these resolvers will be standardized and provided by hyperlane, but community developers with the right motivation could create their own or contribute back new features. In fact, this allows more complex features to be added to the platform progressively as all you need to do is upgrade existing resolvers.

The default resolver will perform the following:

  • All new revisions will be stored permanently in a subdomain following <rev_number>.*.hyperlane.eth
    • These revisions will be accessible permanently at <rev_number>.*.hyperlane.io
    • revision 0 will, by default, point to an "instruction/it works" page with getting started for quick onboarding.
  • The top level domain *.hyperlane.eth will be the "public" version that will default to latest revision
  • top level domain can be overridden with another smart contract for more advanced configuration
    • This is how things like b/g deploys, split testing, and rollouts can be done across revisions
  • Custom alias/subdomains can be made for revisions
    • Allows for human readable routing and abstractions, like creating a "blue" and a "green" alias that the top level router knows how to handle

CLI

A CLI (likely installed as a global npm dependency) will be provided to act as an abstraction away from the smart contract setup and implementation. Assuming a robust and feature rich set of default smart contracts, your average user will only need to register a subdomain of hyperlane.eth by deploying a set of contracts that they will own, and then interacting with them.

The CLI will provide that interface while allowing advanced usages of forking and overwriting default contract behavior.

Upgradability and migrations across cli/smart contract versions will be important to take into consideration.

Possible Examples:

Deploy a new version of your website from your build directory

$ hyperlane publish ./build/out

Change which alias gets default traffic

$ hyperlane set-public-alias green

Perform a blue green deploy, with a contract set to use the alias green instead of latest

# Publish new version aliased as `blue`
$ hyperlane publish ./build/out --alias blue
# run your integration tests
$ yarn integration blue.websitename.hyperlane.io
# swap your blue and green alias so that the newly deployed `blue` is now `green`
$ hyperlane swap-alias blue green

The Gateway

Edit: Still under investigation if needed, eth.show and .luxe domains might be able to solve this natively.

The gateway's goal is to fill a hole in the existing ecosystem. Natively, browsers don't know how to handle

When a user visits a hyperlane website the gateway will:

  1. Map the host to a ENS record.
    • Either via subdomains or a _dnslink DNS text entry for custom domains
    • Cloudflare as _dnslink inspiration
  2. Resolve the ENS record to a content hash, eg. /ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco.
  3. Proxies the request through an IPFS gateway.
    • Likely infura at first, moving to nearby self-hosted caching node later for speed

image

For immutable content types (js, css, images) a file with a format like bundle.<contentHash>.js or with a query bundle.js?hash=<contentHash> will be short circuited to the IPFS gateway.

Move soy gateway to mainnet

Overview

As a developer using soy, I want the protections and tamper-proofing capabilities that main net gives, so that I can have assurance that my website deployed stays there.

Reference

  • Depends #41 for permissioning etc.

Questions

  • List any questions?
  • That you are unsure of the answer?

Assumptions

  • Web3Studio will be deployed to Rinkby, it needs to be moved first
  • In "production" the app should have just-enough protections but nothing over the top
  • Lightweight monitoring to know when gateway is down or acting up
  • Monitoring done with cloudwatch => Slack

Acceptance

  • web3studio.eth.soy shows the build of webstudio-website refereincing the contract on mainnet.
  • Dev team is notified via slack if the website is down or acting up.

Tasks

  • Add monitoring to consensys.net/web3studio/
  • Add monitoring to web3studio.eth.soy/web3studio/
  • Deploy soy-contracts to mainnet and update rinkby and ropsten
    • Update soy contract deployed addrs (Can the build do this? maybe with a
  • Configure website health check deployment to point to prod
  • Configure soy health check and SAM deployment to point to prod
    • Move the domain across accountshard coded migration contract?)
  • Register web3studio.eth.soy to mainnet SoyPublicResolver
  • Configure gateway to point at mainnet
  • Publish story on Medium
  • Update Website to include collection project

Make aliases and revisions accessible as subnodes

Overview

As a user-developer, I want to be able to easily access my revisions and aliases so that I can trace history, future, and perform operations like blue/green deploys.

Acceptance

  • When a revision is published or alias set, resolve that hash as a subnode of the original
    • if default alias resolves as barlock.eth. then rev 1 resolves as 1.barlock.eth and alias blue resolves as blue.barlock.eth

Tasks

  • Update public resolver contract

Allow domain branded error pages

Overview

As a user getting an error page, I want to see a branded error page from the site I'm looking at so that I'm not confused about what and where my browser is

Reference

  • #25 - General custom error pages

Questions

  • Not sure if this is possible...

Assumptions

  • There seems to be a standard for root level documents like 404.html at least in Github Pages. That should be the api if at all possible

Acceptance

  • Site creators can easily declare an error pages with <status_code>.html

Tasks

Embed default infura api key for easy static use.

Overview

As a developer looking to make quick use of soy, I don't want to set up infura so that I can focus on what I'm doing.

Reference

Questions

  • List any questions?
  • That you are unsure of the answer?

Assumptions

  • List any assumptions that
  • You are making in terms of timeframe,
  • Output, or general context setting

Acceptance

  • List of what needs to be true
  • to consider this done
  • done done

Tasks

  • A list of tasks that need
  • to be done to call this issue done

Investigate existing gatways

Overview

As a soy developer, I want to understand how ens resolvers work with existing gateways so that I can understand what changes need to be made, if any, to make soy successful.

Questions

  • What gateway services exist?
  • How do you map custom domains to the services?
  • What kind of latency exists for existing gateways? Around the world?
  • time traveling capabilities?

Acceptance

  • List in this issue of investigated services and outcomes

Tasks

  • Find and list services
  • answer questions per service
  • Create recommendation for #4

Initial ENS resolver

Overview

This is the foundational part of the architecture (#1). It's simply a smart contract that follows the ENS abi with some added features specifically for hyperlane.

Reference

Resolver reference implementation

Acceptance

  • Resolver adheres to EIP 1577 - contentHash field
  • ens records resolve to the contentHash field as an ipfs location /ipfs/Qm...
  • contract keeps track of every website revision
  • contract defines aliases which map to revisions
  • contract defines rev.0 as a dummy website for grins 😁
  • smart contract always resolves the ipfs location of the latest alias
  • contract automatically updates the latest alias to be the last added revision (the largest int)

Tasks

  • Modify reference contract with described features
  • Ensure coverage and linting

Assumptions

  • Migrations will be done with truffle, cli comes later
  • Alias swapping and user defined aliases will be handled later, this is more of a POC to test the whole thing e2e

Initial helper api

Overview

This will deliver a cli and node library for developers to interact with an ens resolver #2. The cli will be a wrapper around the library to enable build scripts in bash and node. The cli will aim to allow for easy contract deployment with the goal of providing a wrapper and management of the deployed contract.

Acceptance

  • there exists a globally installable npm cli
  • Cli is a wrapper around a fully functional node library
  • The node lib can
    • Deploy a new contract
    • help set itself as a resolver?
    • Interact with an existing contract to publish new revisions
  • Coverage and linting

Tasks

  • Create node lib
  • Create cli wrapper

Assumptions

  • web3studio needs a standards lib for prettier/eslint etc config
  • hyperlane is a monorepo (cli, core, contracts, etc)

Use custom error pages in cloudfront

Overview

As a user, I want to see a helpful error page when something goes wrong, so that I'm not confused or become distracted

Reference

Questions

  • There's another issue for domain specific error pages, are those easy to do in this change as well? #26

Acceptance

  • When a user gets a 404, they see a nice looking error page
  • The user never sees a cloudflare specific error page

Tasks

  • Design and create error pages
  • Serve error pages on an error

Setup truffle with reference resolver

Overview

As a developer, I want a base to begin my resolver development so that I can match standards and have a history of what's changed from it

Assumptions

  • Reference resolver is unchanged from existing state

Acceptance

  • Truffle (V5) is setup
  • Unit tests with coverage for solidity

Tasks

  • Copy reference resolver
  • Test test test

Setup monorepo with standards

Overview

As a developer, I want this project to have project standards set by the team enforced automatically

Assumptions

  • config for standards should be in an installable library. (@web3studio/standards or otherwise)

Acceptance

  • Linting via eslint
  • Prettier

Tasks

  • Create standards repo
  • Setup lerna monorepo
  • Setup eslint and prettier with husky for git commits

Cli for normal interactions

Overview

As a web developer using hyperlane, I want a cli for normal operations so that I can easily program my CI logic

Acceptance

  • Wrap all major function of the node lib for cli usage

Tasks

  • Make the cli
  • publish to npm

Soy Core can publish a directory to IPFS and ENS

Overview

As a developer with a new version of a static site, I can upload my content to infura's IPFS so that I'll have a hash I can use to publish.

Assumptions

  • We use infura as a default, users can pass in a different ipfs server

Acceptance

  • Soy has a function that will, given a directory, upload it to ipfs, and return the hash of the root content

Tasks

Initial Gateway

Overview

An initial HTTP gateway enabling ens routing of ipfs locations. Overall logic defined in the architecture #1.

Questions

  • Can .luxe or https://eth.show handle this?
  • Can Cloudflare's _dnslink protocol do this?

Acceptance

  • Users can access their website resolve from ens over HTTP

Nice to haves
These are things that the architecture wants but isn't necessary for the first push. If these can't be accomplished easily, note in this issue what prevents it and or any workarounds.

  • custom domains
  • time traveling across revisions

Tasks

  • Investigate possible services that will do this for us
  • Test resolver with existing gateways (cloudflare, eth.show), see if we can use them
  • reEvaluate and break this down based on answers.

Assumptions

Node library for normal interaction

Overview

As a web developer, I want an easy way to interact with my hyperlane contract in my build system so that I have a quick getting started experience and can use the product without knowing the aetherium ecosystem deeply.

Acceptance

  • Can deploy a new contract
  • help set itself as a resolver?
  • Interact with an existing contract to publish new revisions

Tasks

  • Build the library
  • Deploy to npm

Publish and revision handlers for ENS Resolver

Overview

As a web developer, I want to be able to publish hash locations for my website, so that ens can resolve them and I don't loose history

Acceptance

  • Resolver adheres to EIP 1577 - contentHash field
  • ens records resolve to the contentHash field as an ipfs location /ipfs/Qm...
  • contract keeps track of every website revision
  • contract defines aliases which map to revisions
  • contract defines rev.0 as a dummy website for grins 😁
  • smart contract always resolves the ipfs location of the latest alias
  • contract automatically updates the latest alias to be the last added revision (the largest int)

Tasks

  • Update public resolver contract

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.