Code Monkey home page Code Monkey logo

weight-csv-to-gfit's Introduction

google-fit-data

Load bulk weight/steps data to a Google Fit account

Download and installation

git clone https://github.com/ryanpconnors/google-fit-data.git
cd google-fit-data
virtualenv -p /usr/bin/python2.7 venv
pip install -r requirements.txt

Import weight data into Google Fit

python weight/import_weight_to_gfit.py

Import steps data into Google Fit

python steps/import_steps_to_gfit.py

weight-csv-to-gfit's People

Contributors

chwong1 avatar jayesh100 avatar ryanpconnors 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

weight-csv-to-gfit's Issues

"Unable to fetch DataSource for Dataset:"

Hello,

I've been trying to make this code work again, but my very limited knowledge only got me so far.
By very limited I mean that this is the first time I am interacting with python.

However I managed to fix the oAuth mechanism and some minor issues I had with the code. But I eventually end up in an error, when trying to patch my data into my gfit account.

googleapiclient.errors.HttpError: <HttpError 400 when requesting https://www.googleapis.com/fitness/v1/users/me/dataSources/raw%3Acom.google.weight%3A35873072968%3Aunknown%3Aunknown%3Amaweightimport/datasets/1524024000000000000-1504843200000000000?alt=json&key=AIzaSyA5KxFyd7TlntBDbuags-F4miQo402OG5c returned "Unable to fetch DataSource for Dataset: raw:com.google.weight:35873072968:unknown:unknown:maweightimport">

This is where the error occurs:

    fitness_service.users().dataSources().datasets().patch(
      userId='me',
      dataSourceId=data_source_id,
      datasetId=dataset_id,
      body=dict(
	    minStartTimeNs=min_log_ns,
        maxEndTimeNs=max_log_ns,
        dataSourceId=data_source_id,
        point=weights,
      )).execute()

My current version of the code looks like this (please excuse the excessive amount of prints and commented code. I am a horrible coder):

# -------------------------------------------------------------------------------
# Purpose: Load weights.csv and import to a Google Fit account
# Some codes refer to:
# 1. https://github.com/tantalor/fitsync
# 2. http://www.ewhitling.com/2015/04/28/scrapping-data-from-google-fitness/
import json
import httplib2
import sys
from apiclient.discovery import build
 
from oauth2client.file import Storage
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.tools import run_flow, argparser
from oauth2client.client import AccessTokenRefreshError
from read_weight_csv import read_weights_csv_with_gfit_format
from googleapiclient.errors import HttpError



client_id = '35873072968-gtq30smu2bdnais7qsfs0ccsrb7a8oro.apps.googleusercontent.com'
client_secret = 'K4OuT-VlEtBP4Wd9Y68r9zdF'
scope = 'https://www.googleapis.com/auth/fitness.body.write'



# Setup for Google API:
# Steps: 
# 1. Go https://console.developers.google.com/apis/credentials
# 2. Create credentials => OAuth Client ID
# 3. Set Redirect URI to your URL or the playground https://developers.google.com/oauthplayground
#client_id = '35873072968-gtq30smu2bdnais7qsfs0ccsrb7a8oro.apps.googleusercontent.com'
#CLIENT_SECRET = 'K4OuT-VlEtBP4Wd9Y68r9zdF'
 
# Redirect URI to google Fit, See Steps 3 above
#REDIRECT_URI='https://developers.google.com/oauthplayground'

# See scope here: https://developers.google.com/fit/rest/v1/authorization
#SCOPE = 'https://www.googleapis.com/auth/fitness.body.write'

# API Key
# Steps: 
# 1. Go https://console.developers.google.com/apis/credentials
# 2. Create credentials => API Key => Server Key
API_KEY = 'AIzaSyA5KxFyd7TlntBDbuags-F4miQo402OG5c'

def import_weight_to_gfit():
    # first step of auth
    # only approved IP is my Digital Ocean Server
    flow = OAuth2WebServerFlow(client_id, client_secret, scope)
    storage = Storage('google.json')
    flags = argparser.parse_args([])
    creds = run_flow(flow, storage)
    print "access_token: %s" % creds.access_token
    #run_flow(flow, storage, flags)
    #flow = OAuth2WebServerFlow(client_id=client_id, client_secret=CLIENT_SECRET, scope=SCOPE, redirect_uri=REDIRECT_URI)
    #auth_uri = flow.step1_get_authorize_url()
    #print "Copy this url to web browser for authorization: "
    #print auth_uri

    # hmm, had to manually pull this as part of a Google Security measure. 
    # there must be a way to programatically get this, but this exercise doesn't need it ... yet...
    #token = raw_input("Copy the token from URL and input here: ")
    #cred = flow.step2_exchange(creds.access_token)
    http = httplib2.Http()
    http = creds.authorize(http)
    fitness_service = build('fitness','v1', http=http, developerKey=API_KEY)

    # init the fitness objects
    fitusr = fitness_service.users()
    print "fitnessuser: %s" % fitusr
    fitdatasrc = fitusr.dataSources()
    print "Building the fitness thingies"
    data_source = dict(
        type='raw',
        application=dict(name='maweightimport'),
        dataType=dict(
          name='com.google.weight',
          field=[dict(format='floatPoint', name='weight')]
        ),
        device=dict(
          type='scale',
          manufacturer='unknown',
          model='unknown',
          uid='maweightimport',
          version='1.0',
        )
      )
    print "Getting data sources id"
    def get_data_source_id(dataSource):
      project_number = client_id.split('-')[0]
      return ':'.join((
        dataSource['type'],
        dataSource['dataType']['name'],
        project_number,
        dataSource['device']['manufacturer'],
        dataSource['device']['model'],
        dataSource['device']['uid']))

    print "found data source id: %s" % get_data_source_id(data_source)
    data_source_id = get_data_source_id(data_source)
    # Ensure datasource exists for the device.

			
    print "reading in weights"
    weights = read_weights_csv_with_gfit_format()
    min_log_ns = weights[0]["startTimeNanos"]
    max_log_ns = weights[-1]["startTimeNanos"]
    dataset_id = '%s-%s' % (min_log_ns, max_log_ns)
    print "found data source id: %s" % dataset_id
    # patch data to google fit
    print "test1"
    fitness_service.users().dataSources().datasets().patch(
      userId='me',
      dataSourceId=data_source_id,
      datasetId=dataset_id,
      body=dict(
	    minStartTimeNs=min_log_ns,
        maxEndTimeNs=max_log_ns,
        dataSourceId=data_source_id,
        point=weights,
      )).execute()
    print "test"
    # read data to verify
    print fitness_service.users().dataSources().datasets().get(
        userId='me',
        dataSourceId=data_source_id,
        datasetId=dataset_id).execute()

if __name__=="__main__":
    import_weight_to_gfit()

Which weight unit is expected in CSV

I want to import the past 4 years from Libra to Google Fit. I've checked the example csv file, and because the values are about "180" I'm not sure if this is kilograms or pounds.

Before I'll import wrong data I wanted to ask :)
Thanks!

after copying the tocken I get this error : httplib2.SSLHandshakeError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:727)

Hi,

after copying the token from the url I get this error :

Traceback (most recent call last):
File "weight/import_weight_to_gfit.py", line 128, in
import_weight_to_gfit()
File "weight/import_weight_to_gfit.py", line 51, in import_weight_to_gfit
cred = flow.step2_exchange(token)
File "/home/madwols/.local/lib/python2.7/site-packages/oauth2client/util.py", line 135, in positional_wrapper
return wrapped(*args, **kwargs)
File "/home/madwols/.local/lib/python2.7/site-packages/oauth2client/client.py", line 2117, in step2_exchange
headers=headers)
File "/home/madwols/.local/lib/python2.7/site-packages/httplib2/init.py", line 1609, in request
(response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
File "/home/madwols/.local/lib/python2.7/site-packages/httplib2/init.py", line 1351, in _request
(response, content) = self._conn_request(conn, request_uri, method, body, headers)
File "/home/madwols/.local/lib/python2.7/site-packages/httplib2/init.py", line 1272, in _conn_request
conn.connect()
File "/home/madwols/.local/lib/python2.7/site-packages/httplib2/init.py", line 1059, in connect
raise SSLHandshakeError(e)
httplib2.SSLHandshakeError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:727)

Do I didn't copy the token properly !?
it so please help me figure out which part of the url should I copy ?

Thank you

IndexError: list index out of range

Hi, thank you for the code. Appreciated! I think this is the only way to import Libra CSV at the moment. However, I am trying to import a sample CSV to get the code working. Everything works out until this error appears:

Reading Weights
Traceback (most recent call last):
File "import_weight_to_gfit.py", line 118, in
import_weight_to_gfit()
File "import_weight_to_gfit.py", line 95, in import_weight_to_gfit
min_log_ns = weights[0]["startTimeNanos"]
IndexError: list index out of range

I believe this is due to the CSV format? I am trying to import following CSV content:

2018-08-20 08:48:00;82.9;81.7294;;;

Initially I removed all lines with # symbols (headers) from the file. Timezone is set to "Europe/Zurich" in read_weight_csv.py

How can I fix this issue? Any ideas?

Best wishes,
Alex

HttpError 400 and 409 when running import script for weight

Trying to run the script.. but getting some errors I don't get.

Received following running script for first time:

got weights...
Traceback (most recent call last):
  File "import_weight_to_gfit.py", line 128, in <module>
    import_weight_to_gfit()
  File "import_weight_to_gfit.py", line 118, in import_weight_to_gfit
    point=weights,
  File "/usr/lib/python2.7/site-packages/googleapiclient/_helpers.py", line 134, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/googleapiclient/http.py", line 915, in execute
    raise HttpError(resp, content, uri=self.uri)
googleapiclient.errors.HttpError: <HttpError 400 when requesting https://fitness.googleapis.com/fitness/v1/users/me/dataSources/raw%3Acom.google.weight%3AAAA%3Awithings%3Asmart-body-MASKEDURLBLABLA returned "Unable to fetch DataSource for Dataset: raw:com.google.weight:ABC:DEF:GHI:JKL". Details: "Unable to fetch DataSource for Dataset: raw:com.google.weight:ABC:DEF:GHI:JKL">

Running script for second or more time:

  File "import_weight_to_gfit.py", line 128, in <module>
    import_weight_to_gfit()
  File "import_weight_to_gfit.py", line 101, in import_weight_to_gfit
    body=data_source).execute()
  File "/usr/lib/python2.7/site-packages/googleapiclient/_helpers.py", line 134, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/googleapiclient/http.py", line 915, in execute
    raise HttpError(resp, content, uri=self.uri)
googleapiclient.errors.HttpError: <HttpError 409 when requesting https://fitness.googleapis.com/fitness/v1/users/me/dataSources?alt=json&key=XXXXXX  returned "Data Source: raw:com.google.weight:ABC:DEF:GHI:JKL already exists". Details: "Data Source: raw:com.google.weight:ABC:DEF:GHI:JKL already exists">

First time I did run it, everythings seems fine.. but Google Fit does not display any weights in Google Fit app.
So not sure weither the data was correct imported.

SyntaxError: Missing parentheses in call to 'print'. Did you mean print("Setuptools version",version,"or greater has been installed.")?

Getting this error on
google-fit-data>pip install -r requirements.txt
Collecting astroid==1.4.7 (from -r requirements.txt (line 1))
Using cached https://files.pythonhosted.org/packages/7c/2e/7da23eb111f5086b607fd0fb813fa05932d4d8610f08eb8b9498f3d5628e/astroid-1.4.7-py2.py3-none-any.whl
Collecting google-api-python-client==1.5.1 (from -r requirements.txt (line 2))
Using cached https://files.pythonhosted.org/packages/e4/68/c78710d65b4503bf5d92c1bed96ba7bd49de33d0f453bdc40a7f241f6270/google_api_python_client-1.5.1-py2.py3-none-any.whl
Collecting httplib2==0.9.2 (from -r requirements.txt (line 3))
Using cached https://files.pythonhosted.org/packages/ff/a9/5751cdf17a70ea89f6dde23ceb1705bfb638fd8cee00f845308bf8d26397/httplib2-0.9.2.tar.gz
Collecting isort==4.2.5 (from -r requirements.txt (line 4))
Using cached https://files.pythonhosted.org/packages/c3/0d/917971944e7da7f659e11efe7abf649f0e46c2d45edba2f6d9584ba15374/isort-4.2.5-py2.py3-none-any.whl
Collecting lazy-object-proxy==1.2.2 (from -r requirements.txt (line 5))
Using cached https://files.pythonhosted.org/packages/65/63/b6061968b0f3c7c52887456dfccbd07bec2303296911757d8c1cc228afe6/lazy-object-proxy-1.2.2.tar.gz
Collecting mccabe==0.5.0 (from -r requirements.txt (line 6))
Using cached https://files.pythonhosted.org/packages/59/a2/1382f6a41af561a12914bb3e79edef744872520db42f99347bcdc3af6bbc/mccabe-0.5.0-py2.py3-none-any.whl
Collecting oauth2client==2.2.0 (from -r requirements.txt (line 7))
Using cached https://files.pythonhosted.org/packages/5c/d6/42f18bd74bcc35b3579f08d8b7eb04ee8579b9b16a763b6505f8897c0d6e/oauth2client-2.2.0.tar.gz
Collecting pyasn1==0.1.9 (from -r requirements.txt (line 8))
Using cached https://files.pythonhosted.org/packages/69/be/e8946f7867b84b0e61a613f6fd56f63266190b1a470f945178f7cbc4d0ae/pyasn1-0.1.9-py2.py3-none-any.whl
Collecting pyasn1-modules==0.0.8 (from -r requirements.txt (line 9))
Using cached https://files.pythonhosted.org/packages/10/93/4c08c4d435d42684e552b88a12332bd60a240bc440c29680c67461832cbb/pyasn1_modules-0.0.8-py2.py3-none-any.whl
Collecting pylint==1.6.4 (from -r requirements.txt (line 10))
Using cached https://files.pythonhosted.org/packages/92/f3/41deb50322d579517f779c3421b92f84133ddb6d954791bbd37aca1b5854/pylint-1.6.4-py2.py3-none-any.whl
Collecting python-dateutil==2.5.3 (from -r requirements.txt (line 11))
Using cached https://files.pythonhosted.org/packages/33/68/9eadc96f9899caebd98f55f942d6a8f3fb2b8f8e69ba81a0f771269897e9/python_dateutil-2.5.3-py2.py3-none-any.whl
Collecting rsa==3.4.2 (from -r requirements.txt (line 12))
Using cached https://files.pythonhosted.org/packages/e1/ae/baedc9cb175552e95f3395c43055a6a5e125ae4d48a1d7a924baca83e92e/rsa-3.4.2-py2.py3-none-any.whl
Collecting simplejson==3.8.2 (from -r requirements.txt (line 13))
Using cached https://files.pythonhosted.org/packages/f0/07/26b519e6ebb03c2a74989f7571e6ae6b82e9d7d81b8de6fcdbfc643c7b58/simplejson-3.8.2.tar.gz
Collecting six==1.10.0 (from -r requirements.txt (line 14))
Using cached https://files.pythonhosted.org/packages/c8/0a/b6723e1bc4c516cb687841499455a8505b44607ab535be01091c0f24f079/six-1.10.0-py2.py3-none-any.whl
Collecting uritemplate==0.6 (from -r requirements.txt (line 15))
Using cached https://files.pythonhosted.org/packages/5a/6d/66aed916219c1a25e12a01457ea5442f80e54ed3844ef688b25e20dada5f/uritemplate-0.6.tar.gz
Collecting wrapt==1.10.8 (from -r requirements.txt (line 16))
Using cached https://files.pythonhosted.org/packages/00/dd/dc22f8d06ee1f16788131954fc69bc4438f8d0125dd62419a43b86383458/wrapt-1.10.8.tar.gz
Collecting wsgiref==0.1.2 (from -r requirements.txt (line 17))
Using cached https://files.pythonhosted.org/packages/41/9e/309259ce8dff8c596e8c26df86dbc4e848b9249fd36797fd60be456f03fc/wsgiref-0.1.2.zip
Complete output from command python setup.py egg_info:
Traceback (most recent call last):
File "", line 1, in
File "C:\Users\SUMIT1.CHA\AppData\Local\Temp\pip-install-7uq6ucnp\wsgiref\setup.py", line 5, in
import ez_setup
File "C:\Users\SUMIT
1.CHA\AppData\Local\Temp\pip-install-7uq6ucnp\wsgiref\ez_setup_init_.py", line 170
print "Setuptools version",version,"or greater has been installed."
^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print("Setuptools version",version,"or greater has been installed.")?

----------------------------------------

Command "python setup.py egg_info" failed with error code 1 in C:\Users\SUMIT~1.CHA\AppData\Local\Temp\pip-install-7uq6ucnp\wsgiref
You are using pip version 19.0.1, however version 19.1.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.

C:\asd\google-fit-data>C:\asd\google-fit-data\venv\Scripts\pip install -r requirements.txt
Collecting astroid==1.4.7 (from -r requirements.txt (line 1))
Using cached https://files.pythonhosted.org/packages/7c/2e/7da23eb111f5086b607fd0fb813fa05932d4d8610f08eb8b9498f3d5628e/astroid-1.4.7-py2.py3-none-any.whl
Collecting google-api-python-client==1.5.1 (from -r requirements.txt (line 2))
Using cached https://files.pythonhosted.org/packages/e4/68/c78710d65b4503bf5d92c1bed96ba7bd49de33d0f453bdc40a7f241f6270/google_api_python_client-1.5.1-py2.py3-none-any.whl
Collecting httplib2==0.9.2 (from -r requirements.txt (line 3))
Using cached https://files.pythonhosted.org/packages/ff/a9/5751cdf17a70ea89f6dde23ceb1705bfb638fd8cee00f845308bf8d26397/httplib2-0.9.2.tar.gz
Collecting isort==4.2.5 (from -r requirements.txt (line 4))
Using cached https://files.pythonhosted.org/packages/c3/0d/917971944e7da7f659e11efe7abf649f0e46c2d45edba2f6d9584ba15374/isort-4.2.5-py2.py3-none-any.whl
Collecting lazy-object-proxy==1.2.2 (from -r requirements.txt (line 5))
Using cached https://files.pythonhosted.org/packages/65/63/b6061968b0f3c7c52887456dfccbd07bec2303296911757d8c1cc228afe6/lazy-object-proxy-1.2.2.tar.gz
Collecting mccabe==0.5.0 (from -r requirements.txt (line 6))
Using cached https://files.pythonhosted.org/packages/59/a2/1382f6a41af561a12914bb3e79edef744872520db42f99347bcdc3af6bbc/mccabe-0.5.0-py2.py3-none-any.whl
Collecting oauth2client==2.2.0 (from -r requirements.txt (line 7))
Using cached https://files.pythonhosted.org/packages/5c/d6/42f18bd74bcc35b3579f08d8b7eb04ee8579b9b16a763b6505f8897c0d6e/oauth2client-2.2.0.tar.gz
Collecting pyasn1==0.1.9 (from -r requirements.txt (line 8))
Using cached https://files.pythonhosted.org/packages/69/be/e8946f7867b84b0e61a613f6fd56f63266190b1a470f945178f7cbc4d0ae/pyasn1-0.1.9-py2.py3-none-any.whl
Collecting pyasn1-modules==0.0.8 (from -r requirements.txt (line 9))
Using cached https://files.pythonhosted.org/packages/10/93/4c08c4d435d42684e552b88a12332bd60a240bc440c29680c67461832cbb/pyasn1_modules-0.0.8-py2.py3-none-any.whl
Collecting pylint==1.6.4 (from -r requirements.txt (line 10))
Using cached https://files.pythonhosted.org/packages/92/f3/41deb50322d579517f779c3421b92f84133ddb6d954791bbd37aca1b5854/pylint-1.6.4-py2.py3-none-any.whl
Collecting python-dateutil==2.5.3 (from -r requirements.txt (line 11))
Using cached https://files.pythonhosted.org/packages/33/68/9eadc96f9899caebd98f55f942d6a8f3fb2b8f8e69ba81a0f771269897e9/python_dateutil-2.5.3-py2.py3-none-any.whl
Collecting rsa==3.4.2 (from -r requirements.txt (line 12))
Using cached https://files.pythonhosted.org/packages/e1/ae/baedc9cb175552e95f3395c43055a6a5e125ae4d48a1d7a924baca83e92e/rsa-3.4.2-py2.py3-none-any.whl
Collecting simplejson==3.8.2 (from -r requirements.txt (line 13))
Using cached https://files.pythonhosted.org/packages/f0/07/26b519e6ebb03c2a74989f7571e6ae6b82e9d7d81b8de6fcdbfc643c7b58/simplejson-3.8.2.tar.gz
Collecting six==1.10.0 (from -r requirements.txt (line 14))
Using cached https://files.pythonhosted.org/packages/c8/0a/b6723e1bc4c516cb687841499455a8505b44607ab535be01091c0f24f079/six-1.10.0-py2.py3-none-any.whl
Collecting uritemplate==0.6 (from -r requirements.txt (line 15))
Using cached https://files.pythonhosted.org/packages/5a/6d/66aed916219c1a25e12a01457ea5442f80e54ed3844ef688b25e20dada5f/uritemplate-0.6.tar.gz
Collecting wrapt==1.10.8 (from -r requirements.txt (line 16))
Using cached https://files.pythonhosted.org/packages/00/dd/dc22f8d06ee1f16788131954fc69bc4438f8d0125dd62419a43b86383458/wrapt-1.10.8.tar.gz
Collecting wsgiref==0.1.2 (from -r requirements.txt (line 17))
Using cached https://files.pythonhosted.org/packages/41/9e/309259ce8dff8c596e8c26df86dbc4e848b9249fd36797fd60be456f03fc/wsgiref-0.1.2.zip
ERROR: Complete output from command python setup.py egg_info:
ERROR: Traceback (most recent call last):
File "", line 1, in
File "C:\Users\SUMIT1.CHA\AppData\Local\Temp\pip-install-97ry1rgc\wsgiref\setup.py", line 5, in
import ez_setup
File "C:\Users\SUMIT
1.CHA\AppData\Local\Temp\pip-install-97ry1rgc\wsgiref\ez_setup_init_.py", line 170
print "Setuptools version",version,"or greater has been installed."
^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print("Setuptools version",version,"or greater has been installed.")?
----------------------------------------
ERROR: Command "python setup.py egg_info" failed with error code 1 in C:\Users\SUMIT~1.CHA\AppData\Local\Temp\pip-install-97ry1rgc\wsgiref\

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.