Coder Social home page Coder Social logo

redux-pouchdb's Introduction

redux-pouchdb

How it is done

It is very simple:

  • The PouchDB database persists the state of chosen parts of the Redux store every time it changes.
  • Your reducers will be passed the state from PouchDB when your app loads and every time a change arrives (if you are syncing with a remote db).

Install

yarn add [email protected]

Usage

The reducers to be persisted should be augmented by a higher order reducer accordingly to the type of the state.

Reducers in which the state is an object get persisted as a single document by using persistentDocumentReducer

Reducers in which the state is an array get persisted as a collection where each item is a document by using persistentDocumentReducer

Besides that the store should passed to the plain function persistStore

By following this steps your pouchdb database should keep it self in sync with your redux store.

persistentDocumentReducer

The reducers shaped like as object that you wish to persist should be enhanced with this higher order reducer.

import { persistentDocumentReducer } from 'redux-pouchdb';

const counter = (state = {count: 0}, action) => {
  switch(action.type) {
  case INCREMENT:
    return { count: state.count + 1 };
  case DECREMENT:
    return { count: state.count - 1 };
  default:
    return state;
  }
};

const reducerName = 'counter'
const finalReducer = persistentDocumentReducer(db, reducerName)(reducer)

This is how reducer would be persisted like this

{
  _id: 'reducerName', // the name of the reducer function
  state: {}|[], // the state of the reducer
  _rev: 'x-xxxxx' // pouchdb keeps track of the revisions
}

persistentCollectionReducer

The reducers shaped like as array that you wish to persist should be enhanced with this higher order reducer.

import { persistentCollectionReducer } from 'redux-pouchdb';

const stackCounter = (state = [{ x: 0 }, { x: 1 }, { x: 2 }], action) => {
  switch (action.type) {
    case INCREMENT:
      return state.concat({ x: state.length })
    case DECREMENT:
      return !state.length ? state : state.slice(0, state.length - 1)
    default:
      return state
  }
}

const reducerName = 'stackCounter'
export default persistentCollectionReducer(db, reducerName)(stackCounter)

This is how reducer would be persisted like this

{
  _id: 'reducerName', // the name of the reducer function
  ...state: [], // the state of the reducer
  _rev: 'x-xxxxx' // pouchdb keeps track of the revisions
}

persistStore

This plain function holds the store for later usage

import { persistStore } from 'redux-pouchdb';

const db = new PouchDB('dbname');

const store = createStore(reducer, initialState);
persistStore(store)

waitSync

This function receives a reducerName and returns a promise that resolve true if that reducer is synced

let store = createStore(persistedReducer)
persistStore(store)

store.dispatch({
  type: INCREMENT
})

const isSynced = await waitSync(reducerName)

redux-pouchdb's People

Contributors

colinbate avatar medihack avatar vicentedealencar 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

redux-pouchdb's Issues

Problem with circular change event in pouchDB.

Hello, first of all great job, thanks for this library!
I do not understand why db always triggers change event when I have two persistentDocumentReducer, maybe I'm doing something wrong ?
The problem is only in the "redux-pouchdb" version: "^ 1.0.0-rc.2"
PS: now use version 0.1. * there is no such problem

precondition:
lib version: "redux-pouchdb": "^1.0.0-rc.2",

configureStore
`
import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import { persistStore } from 'redux-pouchdb';
import createReducer from './reducers';

const sagaMiddleware = createSagaMiddleware();

export default function configureStore(initialState = {}, history) {

const middlewares = [
sagaMiddleware,
routerMiddleware(history),
dynamicMiddlewares,
];

const enhancers = [applyMiddleware(...middlewares)];

const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.REDUX_DEVTOOLS_EXTENSION_COMPOSE
? window.REDUX_DEVTOOLS_EXTENSION_COMPOSE({
shouldHotReload: false,
})
: compose;

const store = createStore(
createReducer(),
initialState,
composeEnhancers(...enhancers),
);
persistStore(store);

store.runSaga = sagaMiddleware.run;
store.injectedReducers = {};
store.injectedSagas = {};

if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers));
});
}

return store;
}`

first reducer
`
import { persistentDocumentReducer } from 'redux-pouchdb';
import { appState as appStateDb } from 'configureDb';
import { isNull } from 'lodash';

import {
DEFAULT_ACTION,
SWITCH_CHANGE_ACTION,
CHECKBOX_CHANGE_ACTION,
RESET_DEFAULT_ACTION,
TOGGLE_DIALOG_ACTION,
LOAD_AND_SHOW_RESERVATION_DIALOG_ACTION,
} from './constants';

const initialState = {
loadDefaultAction: true,
checkedIds: [],
switchIds: [],
events: {},
checkedEvents: [],
isDialogLoad: false,
isDialogOpen: false,
reservationRange: null,
};

function reducer(state = initialState, action) {
switch (action.type) {
case RESET_DEFAULT_ACTION: {
return {
...state,
loadDefaultAction: true,
};
}

case DEFAULT_ACTION: {
  const { checkedIds, events, checkedEvents } = action,
    result = {
      ...state,
      checkedIds,
      events,
      checkedEvents,
      loadDefaultAction: false,
    };

  return result;
}

case SWITCH_CHANGE_ACTION: {
  const { checkedIds, switchIds, checkedEvents } = action,
    result = {
      ...state,
      checkedIds,
      switchIds,
    };

  if (!isNull(checkedEvents)) {
    result.checkedEvents = checkedEvents;
  }

  return result;
}

case CHECKBOX_CHANGE_ACTION: {
  const { checkedIds, checkedEvents } = action,
    result = {
      ...state,
      checkedIds,
      checkedEvents,
    };

  return result;
}

case TOGGLE_DIALOG_ACTION: {
  return {
    ...state,
    isDialogOpen: action.value,
  };
}
case LOAD_AND_SHOW_RESERVATION_DIALOG_ACTION: {
  const { isDialogOpen, isDialogLoad, reservationRange } = action;

  return {
    ...state,
    isDialogOpen,
    isDialogLoad,
    reservationRange,
  };
}

default:
  return state;

}
}

const reducerName = 'uniqueReducerName';
export default persistentDocumentReducer(appStateDb, reducerName)(reducer);
`

second reducer

`
import { persistentDocumentReducer } from 'redux-pouchdb';
import { appState as appStateDb } from 'configureDb';
import { SET_TAB_VALUE, HANDLE_DRAWER_TOGGLE } from './constants';

const initialState = {
open: false,
tabValue: 0,
};

function appReducer(state = initialState, action) {
switch (action.type) {
case SET_TAB_VALUE: {
return {
...state,
tabValue: action.value,
};
}

case HANDLE_DRAWER_TOGGLE: {
  return {
    ...state,
    open: !state.open,
  };
}

default:
  return state;

}
}

const reducerName = 'app';

export default persistentDocumentReducer(appStateDb, reducerName)(appReducer);
`

When I turn to the page where two reducers are connected at once (dynamically connected), change event are always triggered

pouchdb:api log

pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms put success {ok: true, id: "apartmentReservationPage", rev: "618-01871ea6836aeb5f595b778fe8a8ecb4"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms get apartmentReservationPage browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +6ms get success {state: {…}, _id: "apartmentReservationPage", _rev: "618-01871ea6836aeb5f595b778fe8a8ecb4"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms put {state: {…}, _id: "apartmentReservationPage", _rev: "618-01871ea6836aeb5f595b778fe8a8ecb4"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +2ms bulkDocs {docs: Array(1)} {} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +16ms bulkDocs success [{…}] browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +0ms put success {ok: true, id: "apartmentReservationPage", rev: "619-2244c2adf65b7a53f9b2d8f7e787643a"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +2ms get apartmentReservationPage browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +5ms get success {state: {…}, _id: "apartmentReservationPage", _rev: "619-2244c2adf65b7a53f9b2d8f7e787643a"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms put {state: {…}, _id: "apartmentReservationPage", _rev: "619-2244c2adf65b7a53f9b2d8f7e787643a"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms bulkDocs {docs: Array(1)} {} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +9ms bulkDocs success [{…}] browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +0ms put success {ok: true, id: "apartmentReservationPage", rev: "620-46d2e402895149c18488ba8a33ce9c59"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +2ms get apartmentReservationPage browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +4ms get success {state: {…}, _id: "apartmentReservationPage", _rev: "620-46d2e402895149c18488ba8a33ce9c59"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +2ms put {state: {…}, _id: "apartmentReservationPage", _rev: "620-46d2e402895149c18488ba8a33ce9c59"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms bulkDocs {docs: Array(1)} {} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +15ms bulkDocs success [{…}] browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +0ms put success {ok: true, id: "apartmentReservationPage", rev: "621-9d4dd0b9d2290c248e0c5ecb0a5fe6cb"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms get apartmentReservationPage browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +5ms get success {state: {…}, _id: "apartmentReservationPage", _rev: "621-9d4dd0b9d2290c248e0c5ecb0a5fe6cb"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms put {state: {…}, _id: "apartmentReservationPage", _rev: "621-9d4dd0b9d2290c248e0c5ecb0a5fe6cb"} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms bulkDocs {docs: Array(1)} {} browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +11ms bulkDocs success [{…}] browser.js:133 pouchdb:api /app-state-522fc10d-a59a-4766-89c2-faa280ab06fd +1ms put success {ok: true, id: "apartmentReservationPage", rev: "622-edcc4b3a150ff7b865687b8a297d5045"}

Add to documentation warning about pouchdb data serialization.

Hello, first of all great job, thanks for this library!

When implemented in my project I came across an unpleasant problem related to the serialization of data in pouchdb.
I tried to save the moment js object in pouchdb and get an error like DataCloneError: Failed to execute 'put' on 'IDBObjectStore': An object could not be cloned.
CustomPouchError {status: 500, name: "indexed_db_went_bad", message: "unknown", error: true, reason: "AbortError", …}

Link to related issue in pouchdb pouchdb/pouchdb#6411

It would be cool to add a reference to this issue in the project documentation.
I think some kind of warning block it would be nice.
Maybe I'll do pr next week, if needed.

State not updated when a "pull" change is received

Hello, not really an issue but a question....

My state is not updated when a pull change (new doc) is received from remote Pouchdb. My reducer is a persistentCollectionReducer and while debugging I see an action of type '@@redux-pouchdb/UPDATE_ARRAY_REDUCER' is dispatched to my reducer. Should I implement that switch case in order to update my state? I thought the lib was doing that for us. Am I wrong?

Thanks for the support

Black/whitelist store keys

is there a way to decide what part of each reducer will be persisted?
e.g:
{ a: [], b[] }
and only want 'a' to be synced with the DB

Anonymous reducer function will crash `persistentReducer`

Hey @vicentedealencar,

First, thanks for redux-pouchdb, it's really been a breeze to use!

Until recently, when I passed it a reducer obtained through a mechanism that yielded an anonymous function (unassigned arrow function, FYI). Took me a good while to track this down.

See, on this line of index.js you’re creating the "reducer name" that you'll later use as _id for your doc, esp. on initial save, unless an explicit name was passed as second argument to persistentReducer.

Obviously, when the passed reducer is anonymous and there's no second argument passed (I didn't even know you could pass one), it'll just burn in flames when trying a db.put. You'll get a 412 missing_id PouchDB error.

I think the best strategy here would be to throw as soon as you realize the to-be-id is empty, or even blank. Perhaps include advice in the exception's message about the possible fixes?

Best,

Using with Electron

If i use redux-pouchdb in Electron, and after every starting app and adding new item to store see errors in console:
Warning: flattenChildren(...): Encountered two children with the same key,.0:$0. Child keys must be unique; when two children share a key, only the first child will be used.

And every time after adding new items in store and after restart app in console message reduce this .. array of object growing together with error message Warning: flattenChildren(...): almost 2-fold and items in store alternate.

Entire state object gets persisted in each reducer

My setup is as follows:

createRootReducer(){
   return combineReducers({
     auth: authReducer,
     creds: credsReducer,
     content: persistentDocumentReducer(db, 'content')(contentReducer),
   })
}

I am running into an issue where the only thing I want to persist in the db is content but the entire state gets written to db including auth and creds which I have not included in the persistentDocumentReducer

Any ideas?

Some error in README.md

This is the current usage part of README.md. I have found two error.

  • Noticed the blod text persistentDocumentReducer below. According to the usage example, it should be persistentCollectionReducer.
  • The parameter reducer in the example code block should be counter.

Usage
The reducers to be persisted should be augmented by a higher order reducer accordingly to the type of the state.

Reducers in which the state is an object get persisted as a single document by using persistentDocumentReducer

Reducers in which the state is an array get persisted as a collection where each item is a document by using persistentDocumentReducer <-- the first error

Besides that the store should passed to the plain function persistStore

By following this steps your pouchdb database should keep it self in sync with your redux store.


persistentDocumentReducer
The reducers shaped like as object that you wish to persist should be enhanced with this higher order reducer.

import { persistentDocumentReducer } from 'redux-pouchdb';

const counter = (state = {count: 0}, action) => {
  switch(action.type) {
  case INCREMENT:
    return { count: state.count + 1 };
  case DECREMENT:
    return { count: state.count - 1 };
  default:
    return state;
  }
};

const reducerName = 'counter'
const finalReducer = persistentDocumentReducer(db, reducerName)(reducer)  //<-- the second error

CustomPouchError - Bad Request - Document must be a JSON Object

This error happens when 2 elements are added to the Redux store in rapid succession. The Context in saveArray somehow ends up in prev: 30, next: 30. See screeenshot of console below.

D:\projects\notoriou…ls\saveArray.js:122 
CustomPouchError {status: 400, name: "bad_request", message: "Document must be a JSON object", error: true}
status: 400
name: "bad_request"
message: "Document must be a JSON object"
error: true
__proto__: Error

image

Any idea what is causing this? I can reproduce it consistently any help how to further investigate would be appreciated.

Debug logging

Currently, a lot of the debug console.logs are cluttering up my workspace (ex: http://i.imgur.com/xuTUaVM.png).

Can the master branch be devoid of logs, or at least have some sort of debug flag? Thank you!

Performance question

Hey!

I need some way to sync my database to redux and found this library. It looks like you write to the db each time anything within the state you watch changes. If the state can change quite frequently, wouldn't this hit the performance a lot by requesting so many write operations to the filesystem?

persistentCollectionReducer: Only top level objects persisted.

I have a following contrived reducer where state resulting from ADD_TODO action is correctly persisted. However, state resulting from COMPLETED action is not persisted. As you can see my initialState is [ ]. FWIW, state in redux is as expected but not synced with db for COMPLETED action.

import { persistentCollectionReducer } from "redux-pouchdb";
import PouchDB from "pouchdb-browser";

const db = new PouchDB("todos");

const initialState = [];

const todosReducer = (state = initialState, { type, payload }) => {
  switch (type) {
    case "ADD_TODO":
      return [
        ...state,
        {
          _id: Math.random().toString(8),
          ...payload,
        },
      ];
    case "COMPLETED":
      return state.map((item) => {
        if (item._id === payload.id) {
          item.completed = !item.completed;
        }
        return item;
      });

    default:
      return state;
  }
};
export default persistentCollectionReducer(db, "todos")(todosReducer);

Question: Switch remote database in runtime

Hello!!

I develop an app which might sync to different remote Couchdb database. The database can be changed by user select option. Could you please explain how to switch remote database at runtime?

Any help would be appreciated!!

Problem while setting up the development environment.

I would like to contribute some small enhancements to your library. Unfortunately I have problems setting thinks up.

When I install the dependencies with npm install it crashes with

npm ERR! Linux 3.13.0-74-generic
npm ERR! argv "/home/kai/.nvm/versions/node/v5.5.0/bin/node" "/home/zeus/.nvm/versions/node/v5.5.0/bin/npm" "install"
npm ERR! node v5.5.0
npm ERR! npm  v3.3.12
npm ERR! code ELIFECYCLE
npm ERR! [email protected] prepublish: `rimraf lib && babel src --out-dir lib`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the [email protected] prepublish script 'rimraf lib && babel src --out-dir lib'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the redux-pouchdb package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     rimraf lib && babel src --out-dir lib
npm ERR! You can get their info via:
npm ERR!     npm owner ls redux-pouchdb
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /home/kai/Projects/redux-pouchdb/npm-debug.log

When I call rimraf lib && babel src --out-dir lib myself I get this error:

SyntaxError: src/save.js: Unexpected token (22:8)
  20 |     return loadReducer(reducerName).then(doc => {
  21 |       const newDoc = {
> 22 |         ...doc
     |         ^
  23 |       };
  24 | 
  25 |       if (Array.isArray(reducerState)) {
zeus@olympus2:~/Projects/redux-pouchdb$ npm install

I using current Node and Babel.

Any idea? I must admit that I also never used the spread operator (...) in that way.

Wrong comparison with store

Are you sure this comparison is correct in https://github.com/vicentedealencar/redux-pouchdb/blob/master/src/index.js#L45
It seems to me that you compare the whole state of the store to a reducer state (mostly the reducer state is just one part of the store state).
But even when comparing with the reducer state it will be quite tricky cause of some racing conditions. I solved this problem (at least I hope so) by filtering out local db changes (see http://stackoverflow.com/questions/28280276/changes-filter-only-changes-from-other-db-instances).

Can't use with pouchdb-react-native.

Hi,
when import persistentStore, throw the next error:

Code:

import { persistentStore } from 'redux-pouchdb';

426327587_100043_17744933179950334863


Test in Android
redux-pouchdb: ^0.1.1
pouchdb-react-native: ^6.1.18

Regards

Multiple persistent reducers override reducers state

Hi,

The state of my reducers is overridden with one of the reducers state if there is more than one persistent reducer.

This is what I have in my root reducer:

pouchdb => combineReducers({
  localize,
  authentication,
  financialForecast: FinancialForecastReducer,
  wallets: persistentDocumentReducer(pouchdb, 'wallets')(walletsReducer),
  tags: persistentDocumentReducer(pouchdb, 'tags')(tagsReducer),
  rules: rulesReducer,
  contracts: contractsReducer,
  budgets: budgetsReducer,
})

I debugged and found that the middleware is initializing both reducers with the two documents (wallets and tags) in pouchdb. As these are synchronous actions both end with reducer in the same state.

The next code change in persistentObjectReducer fixes this issue.

const setReducer = (store, doc, reducerName) => {
  const { _id, _rev, state } = doc
  if (_id === reducerName) {
    store.dispatch({
      type: SET_OBJECT_REDUCER,
      reducerName, //_id,
      state,
      _rev
    })
  }
}

I added the condition to prevent the reducer from being wrongly initialized.

If you feel this code doesn't have any implications in the rest of the library I can create a PR.

storeCreator is not a function

I followed the steps in the README, but am getting these errors:

reduce this undefined { type: '@@redux/INIT' }
reducedState { count: 0 }
reduce this undefined { type: '@@redux/PROBE_UNKNOWN_ACTION_i.2.v.e.l.j.t.t.9' }
reducedState { count: 0 }
reduce this undefined { type: '@@INIT' }
reducedState { count: 0 }
/Users/project/node_modules/redux-pouchdb/lib/index.js:30
      var store = storeCreator(reducer, initialState);
                  ^
TypeError: storeCreator is not a function
    at /Users/project/node_modules/redux-pouchdb/lib/index.js:30:19

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.