Git Product home page Git Product logo

syntaxnet-rest-api's Introduction

Syntaxnet Rest API

This is a simple Rest API for Google Syntaxnet. It parse the string with syntaxnet and return a json for you

The server uses Flask-restful / uwsgi and nginx, so it should be okay for multi query at the same time ( To be tested)

The Version you're checking is using the latest DRAGNN mode, which is way more faster than the original one!

Usage

docker run -p 9000:9000 -v /test_folder:/models -d ljm625/syntaxnet-rest-api:dragnn

Look here for detail: https://github.com/tensorflow/models/blob/master/syntaxnet/g3doc/conll2017/README.md

Download the conll2017 from here: https://drive.google.com/file/d/0BxpbZGYVZsEeSFdrUnBNMUp1YzQ/view?usp=sharing

Then extract the file and put the language folder you're using into a folder. (we use /test_folder here)

execute the command like this:

docker run -p 9000:9000 -v /test_folder:/models -d ljm625/syntaxnet-rest-api:dragnn

then GET to http://localhost:9000/api/v1/use/*the_folder_name*

for example, I am using English package, and the folder we extracted is called English, so the path is like /test_folder/English, and the url should be http://localhost:9000/api/v1/use/English

The command above will load the model and let you able to use the module

then POST to http://localhost:9000/api/v1/query

The Body of the POST is a json consisting following info:

{
   "strings": ["Google is awesome!","Syntaxnet is Cool"],
   "tree": true/false
}

The TREE option determines whether the output format is like a tree or just some lists, please check the demo below

and you should expect a response instantly.

Response with tree:false

[
  {
    "input": "Google is awesome!",
    "output": [
      {
        "category": "",
        "pos_tag": "NNP",
        "head": 2,
        "word": "Google",
        "break_level": 0,
        "fPOS": "PROPN++NNP",
        "Number": "Sing",
        "label": "nsubj"
      },
      {
        "category": "",
        "pos_tag": "VBZ",
        "head": 2,
        "word": "is",
        "Mood": "Ind",
        "break_level": 1,
        "fPOS": "AUX++VBZ",
        "Number": "Sing",
        "label": "cop",
        "Person": "3",
        "Tense": "Pres",
        "VerbForm": "Fin"
      },
      {
        "category": "",
        "pos_tag": "JJ",
        "head": -1,
        "word": "awesome",
        "Degree": "Pos",
        "break_level": 1,
        "fPOS": "ADJ++JJ",
        "label": "root"
      },
      {
        "category": "",
        "pos_tag": ".",
        "head": 2,
        "word": "!",
        "break_level": 0,
        "fPOS": "PUNCT++.",
        "label": "punct"
      }
    ]
  },
  {
    "input": "Syntaxnet is Cool",
    "output": [
      {
        "category": "",
        "pos_tag": "NN",
        "head": 2,
        "word": "Syntaxnet",
        "break_level": 0,
        "fPOS": "NOUN++NN",
        "Number": "Sing",
        "label": "nsubj"
      },
      {
        "category": "",
        "pos_tag": "VBZ",
        "head": 2,
        "word": "is",
        "Mood": "Ind",
        "break_level": 1,
        "fPOS": "AUX++VBZ",
        "Number": "Sing",
        "label": "cop",
        "Person": "3",
        "Tense": "Pres",
        "VerbForm": "Fin"
      },
      {
        "category": "",
        "pos_tag": "JJ",
        "head": -1,
        "word": "Cool",
        "Degree": "Pos",
        "break_level": 1,
        "fPOS": "ADJ++JJ",
        "label": "root"
      }
    ]
  }
]

Response with tree:true

[
  {
    "category": "",
    "pos_tag": "JJ",
    "word": "awesome",
    "Degree": "Pos",
    "break_level": 1,
    "contains": [
      {
        "category": "",
        "pos_tag": "NNP",
        "word": "Google",
        "break_level": 0,
        "fPOS": "PROPN++NNP",
        "Number": "Sing",
        "label": "nsubj"
      },
      {
        "category": "",
        "pos_tag": "VBZ",
        "word": "is",
        "Mood": "Ind",
        "break_level": 1,
        "fPOS": "AUX++VBZ",
        "Number": "Sing",
        "label": "cop",
        "Person": "3",
        "Tense": "Pres",
        "VerbForm": "Fin"
      },
      {
        "category": "",
        "pos_tag": ".",
        "word": "!",
        "break_level": 0,
        "fPOS": "PUNCT++.",
        "label": "punct"
      }
    ],
    "fPOS": "ADJ++JJ",
    "label": "root"
  },
  {
    "category": "",
    "pos_tag": "JJ",
    "word": "Cool",
    "Degree": "Pos",
    "break_level": 1,
    "contains": [
      {
        "category": "",
        "pos_tag": "NN",
        "word": "Syntaxnet",
        "break_level": 0,
        "fPOS": "NOUN++NN",
        "Number": "Sing",
        "label": "nsubj"
      },
      {
        "category": "",
        "pos_tag": "VBZ",
        "word": "is",
        "Mood": "Ind",
        "break_level": 1,
        "fPOS": "AUX++VBZ",
        "Number": "Sing",
        "label": "cop",
        "Person": "3",
        "Tense": "Pres",
        "VerbForm": "Fin"
      }
    ],
    "fPOS": "ADJ++JJ",
    "label": "root"
  }
]

Feel free to try different languages using the prebuilt models :D

Special Thanks

Special Thanks to malahovKS for testing and submitting issues, really appreciated!

This repo uses uses tiangolo's uwsgi+nginx+supervisord dockerfile. Special Thanks to him

Updates

2017/03/28: Rewrite most of the part of the code, using DRAGNN which is faster than before, also some minor bug fixes.

2017/02/28: Using another method for fetching the info from syntaxnet engine, so you can get lots of info using custom model than before :D

2017/02/26: Fix the issue with the UTF8 encoding, so non-lantern language are supported

2016/11/18: Update the logic for multi sentence query so it should faster now

2016/11/17: Updated the syntaxnet repo with the latest code, with working parser_universal and muti-language support.

If you have any questions, feel free to ask in the discussion part ;)

syntaxnet-rest-api's People

Contributors

ljm625 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

syntaxnet-rest-api's Issues

404 when trying to use other language model

I'm trying to follow the instructions and have both tried to mount volume and add the files to the container in a separate Dockerfile but I get the same error (404). I suspect the error comes from nginx so maybe there is something else needed to allow queries to the language subpath?

curl -X POST -d '{ "strings": ["De äter pizza med ansjovis!","Knut satt på en knut och knöt en knut"] }' -H "Content-Type: application/json" http://localhost:9000/api/v1/query/Swedish

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again.</p>

Some issues in response format (DRAGNN branch).

Some issues in response format (DRAGNN branch).

  1. For the convenience of parsing, cast the names of fields to lowercase
    for example this
    { "category": "", "Case": "Nom", "head": -1, "Animacy": "Inan", "word": "системы", "break_level": 1, "pos_tag": "NN", "fPOS": "NOUN++NN", "Number": "Plur", "label": "root", "Gender": "Fem" }
    to this
    { "category": "", "case": "Nom", "head": -1, "animacy": "Inan", "word": "системы", "breakLevel": 1, "posTag": "NN", "fPOS": "NOUN++NN", "number": "Plur", "dep": "root", "gender": "Fem" }
  2. change the name of field "label" to "dep" (that's field means "dependency" according universaldependencies.org)
  3. The most important There is no field containing part of the speech of the word. In master branch with old version it was "pos" field - "pos": "VERB"

Getting wrong JSON output

I am running the syntaxnet-rest-api docker container and when I POST to http://localhost:9000/api/v1/query/English with data { "strings": ["Google is awesome!","Syntaxnet is Cool"] }.
I am getting the wrong output:

OUTPUT:
[{"pos_tag": "NNP", "name": "G", "dep": "ROOT", "fPOS": "PROPN++NNP", "Number": "Sing", "pos": "PROPN"}, {"pos_tag"
: "UH", "name": "o", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "UH", "name": "o", "dep": "ROOT
", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "NNP", "name": "g", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "P
ROPN"}, {"pos_tag": "UH", "name": "l", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "UH", "name":
"e", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "UH", "name": "i", "dep": "ROOT", "fPOS": "INT
J++UH", "pos": "INTJ"}, {"pos_tag": "UH", "name": "s", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag
": "DT", "PronType": "Art", "dep": "ROOT", "Definite": "Ind", "fPOS": "DET++DT", "pos": "DET", "name": "a"}, {"pos_
tag": "UH", "name": "w", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "UH", "name": "e", "dep": "
ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "UH", "name": "s", "dep": "ROOT", "fPOS": "INTJ++UH", "pos":
"INTJ"}, {"pos_tag": "UH", "name": "o", "dep": "ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": "NN", "name
": "m", "dep": "ROOT", "fPOS": "NOUN++NN", "Number": "Sing", "pos": "NOUN"}, {"pos_tag": "UH", "name": "e", "dep":
"ROOT", "fPOS": "INTJ++UH", "pos": "INTJ"}, {"pos_tag": ".", "name": "!", "dep": "ROOT", "fPOS": "PUNCT++.", "pos":
"PUNCT"}]

"500 Internal Server Error" when trying to use custom language model

When trying to use custom language model
receive 500 Internal Server Error from the server

list index out of range
[2017-03-01 07:26:51,680] ERROR in app: Exception on /api/v1/query/English [POST]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1639, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1625, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py", line 481, in wrapper
    return self.make_response(data, code, headers=headers)
  File "/usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py", line 510, in make_response
    resp = self.representations[mediatype](data, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/flask_restful/representations/json.py", line 20, in output_json
    dumped = dumps(data, **settings) + "\n"
  File "/usr/lib/python2.7/json/__init__.py", line 243, in dumps
    return _default_encoder.encode(obj)
  File "/usr/lib/python2.7/json/encoder.py", line 207, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python2.7/json/encoder.py", line 270, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python2.7/json/encoder.py", line 184, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: IndexError('list index out of range',) is not JSON serializable
[pid: 9|app: 0|req: 1/1] 192.168.1.103 () {40 vars in 565 bytes} [Wed Mar  1 07:26:36 2017] POST /api/v1/query/English => generated 37 bytes in 15486 msecs (HTTP/1.1 500) 2 headers in 90 bytes (1 switches on core 0)

DRAGNN Chinese segmenter : No such file or directory

Hi, I could run the syntaxnet docker with Chinese model. Now I am using DRAGNN docker, and got following error when calling

ip/api/v1/use/Chinese

{"reason": "[Errno 2] No such file or directory: '/models/Chinese/segmenter/spec.textproto'", "result": "fail"}

Parsing returns individual characters instead of words.

When running a query, it seems that syntaxnet is parsing the letters , not words.

[user@localhost ~]$ curl -H "Content-Type: application/json" -X POST -d '{"strings":"Bob"}' http://localhost:9000/api/v1/query

Returns ->
[
{
"category": "",
"pos_tag": "NNP",
"word": "B",
"dep": "root",
"break_level": 0,
"fpos": "PROPN++NNP",
"number": "Sing",
"label": "root"
},
{
"category": "",
"case": "Nom",
"word": "o",
"prontype": "Prs",
"dep": "root",
"break_level": 0,
"pos_tag": "PRP",
"fpos": "PRON++PRP",
"number": "Sing",
"label": "root",
"person": "1"
},
{
"category": "",
"pos_tag": "LS",
"word": "b",
"dep": "root",
"break_level": 0,
"fpos": "X++LS",
"label": "root"
}
]

Error when querying DRAGNN over the API

I'm running the DRAGNN docker image, and doing the following requests:

Init the model (works fine):

curl -X GET http://localhost:9000/api/v1/use/English 

returns

{
    "result": "success"
}

Furthermore, querying the model with a single word (basically same issue as #10):

curl -X POST \
  http://localhost:9000/api/v1/query \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -d '{
   "strings": ["Google"],
   "tree": false
}'

outputs

[
    {
        "input": "G",
        "output": [
            {
                "category": "",
                "pos_tag": "NNP",
                "head": -1,
                "word": "G",
                "dep": "root",
                "break_level": 0,
                "fpos": "PROPN++NNP",
                "number": "Sing",
                "label": "root"
            }
        ]
    },
    {
        "input": "o",
        "output": [
            {
                "category": "",
                "case": "Nom",
                "head": -1,
                "word": "o",
                "prontype": "Prs",
                "dep": "root",
                "break_level": 0,
                "pos_tag": "PRP",
                "fpos": "PRON++PRP",
                "number": "Sing",
                "label": "root",
                "person": "1"
            }
        ]
    },
    {
        "input": "o",
        "output": [
            {
                "category": "",
                "case": "Nom",
                "head": -1,
                "word": "o",
                "prontype": "Prs",
                "dep": "root",
                "break_level": 0,
                "pos_tag": "PRP",
                "fpos": "PRON++PRP",
                "number": "Sing",
                "label": "root",
                "person": "1"
            }
        ]
    },
    {
        "input": "g",
        "output": [
            {
                "category": "",
                "pos_tag": "LS",
                "head": -1,
                "word": "g",
                "dep": "root",
                "break_level": 0,
                "fpos": "X++LS",
                "label": "root"
            }
        ]
    },
    {
        "input": "l",
        "output": [
            {
                "category": "",
                "pos_tag": "NNP",
                "head": -1,
                "word": "l",
                "dep": "root",
                "break_level": 0,
                "fpos": "PROPN++NNP",
                "number": "Sing",
                "label": "root"
            }
        ]
    },
    {
        "input": "e",
        "output": [
            {
                "category": "",
                "pos_tag": "UH",
                "head": -1,
                "word": "e",
                "dep": "root",
                "break_level": 0,
                "fpos": "INTJ++UH",
                "label": "root"
            }
        ]
    }
]

However, if I pass in multiple words like this:

curl -X POST \
  http://localhost:9000/api/v1/query \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -d '{
   "strings": ["Google is awesome!"],
   "tree": false
}'

I get output

<html>
    <head>
        <title>502 Bad Gateway</title>
    </head>
    <body bgcolor="white">
        <center>
            <h1>502 Bad Gateway</h1>
        </center>
        <hr>
        <center>nginx/1.6.2</center>
    </body>
</html>

and the following error in the console:

[pid: 783|app: 0|req: 6/6] 172.17.0.1 () {42 vars in 588 bytes} [Tue Sep 19 16:41:02 2017] POST /api/v1/query => generated 1126 bytes in 1521 msecs (HTTP/1.1 200) 3 headers in 105 bytes (1 switches on core 2)
172.17.0.1 - - [19/Sep/2017:16:41:03 +0000] "POST /api/v1/query HTTP/1.1" 200 1126 "-" "PostmanRuntime/6.3.2"
Annotating: G
['Number']
['fPOS']
Annotating: o
['Case']
['Number']
['Person']
['PronType']
['fPOS']
Annotating: o
['Case']
['Number']
['Person']
['PronType']
['fPOS']
Annotating: g
['fPOS']
Annotating: l
['Number']
['fPOS']
Annotating: e
['fPOS']
Annotating:
2017/09/19 16:42:13 [error] 9#0: *20 upstream prematurely closed connection while reading response header from upstream, client: 172.17.0.1, server: , request: "POST /api/v1/query HTTP/1.1", upstream: "uwsgi://unix:///tmp/uwsgi.sock:", host: "localhost:9000"
2017-09-19 16:42:13,228 INFO exited: uwsgi (terminated by SIGSEGV; not expected)
172.17.0.1 - - [19/Sep/2017:16:42:13 +0000] "POST /api/v1/query HTTP/1.1" 502 172 "-" "PostmanRuntime/6.3.2"
2017-09-19 16:42:13,257 INFO spawned: 'uwsgi' with pid 1554
[uWSGI] getting INI configuration from /etc/uwsgi/uwsgi.ini
[uWSGI] getting INI configuration from /app/uwsgi.ini
*** Starting uWSGI 2.0.15 (64bit) on [Tue Sep 19 16:42:13 2017] ***
compiled with version: 4.9.2 on 19 September 2017 08:42:15
os: Linux-4.9.41-moby #1 SMP Wed Sep 6 00:05:16 UTC 2017
nodename: 304cad302a2f
machine: x86_64
clock source: unix
detected number of CPU cores: 2
current working directory: /app
detected binary path: /usr/local/bin/uwsgi
!!! no internal routing support, rebuild with pcre support !!!
*** WARNING: you are running uWSGI without its master process manager ***
your memory page size is 4096 bytes
detected max file descriptor number: 1048576
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to UNIX address /tmp/uwsgi.sock fd 3
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
Python version: 2.7.9 (default, Jun 29 2016, 13:11:10)  [GCC 4.9.2]
Python main interpreter initialized at 0x1114f70
python threads support enabled
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 103648 bytes (101 KB) for 4 cores
*** Operational MODE: threaded ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 1554, cores: 4)
2017-09-19 16:42:14,450 INFO success: uwsgi entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
WSGI app 0 (mountpoint='') ready in 2 seconds on interpreter 0x1114f70 pid: 1554 (default app)

I've tried building the images myself, as well as using the pre-compiled ones. Same issue.

I've also tested this on multiple Ubuntu machines, both locally and hosted on Google Cloud.

I wonder if there is a part that needs to be set separately when trying to insert another language pack.

I am testing syntexnet applying Korean using your source.
In the dockerfile, I wonder that there is no part to learn with the language set.
Looking at the source, it seems that the shell script located in parser.py works by referring to the path of the config file.
Custom_parse.sh seems to be similar to demo.sh which is automatically generated by syntexnet.

I wonder if there is a part that needs to be set separately when trying to insert another language pack.

"500 Internal Server Error" when trying to use Russian-SynTagRus language model.

I'm trying to use Russian-SynTagRus language model and get 500 Internal Server Error.
I think there is some encoding issues with cyrillic symbols processing.
Most likely for processing strings with Cyrillic characters need to use Unicode, something like that for string processing:

data = u"""
Привет , как твои дела ?
Использование кириллических символов .
"""

or in Dockerfile to setup locales

RUN \
echo u_RU.UTF-8 UTF-8 > /etc/locale.gen && \
locale-gen "ru_RU.UTF-8" && \
echo 'LANG="ru_RU.UTF-8"'>/etc/default/locale && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LC_ALL=ru_RU.UTF-8 LANG=ru_RU.UTF-8  

ENV LANG ru_RU.UTF-8

Server CORS (CROSS-ORIGIN RESOURCE SHARING) requests handling issue (DRAGNN branch).

Server CORS (CROSS-ORIGIN RESOURCE SHARING) requests handling issue (DRAGNN branch).

There will be great to add possibility to server to handling CORS requests.
In my case, the parsing server is working behind a specially designed queue server, and there is no such big problem. But to work with the parsing server directly, you need to add headers in server code that includes CORS support:
('Access-Control-Allow-Origin: *'); ('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS'); ('Access-Control-Allow-Headers: Content-Type, Content-Range, Content-Disposition, Content-Description');

Question about JSON output data

Hi,
Anyone knows where can I find the definition of output data? e.g. what does element "break_level" : 0 mean? Thanks.

    "category": "",
    "pos_tag": "NNP",
    "head": 2,
    "word": "Google",
    "break_level": 0,
    "fPOS": "PROPN++NNP",
    "Number": "Sing",
    "label": "nsubj"

add in JSON output format support of the all list of morphological features tags

One of the most important column in CoNLL-U output file is 6 column - List of morphological features
(FEATS: List of morphological features http://universaldependencies.org/u/feat/index.html)

Only pos_tag (fPOS in CoNLL-U) is available in your output json.

List of morphological features tags ( also avaliable here http://universaldependencies.org/u/feat/index.html):

  • Abbr: abbreviation
  • Animacy: animacy
  • Aspect: aspect
  • Case: case
  • Definite: definiteness or state
  • Degree: degree of comparison
  • Evident: evidentiality
  • Foreign: is this a foreign word?
  • Gender: gender
  • Mood: mood
  • NumType: numeral type
  • Number: number
  • Person: person
  • Polarity: polarity
  • Polite: politeness
  • Poss: possessive
  • PronType: pronominal type
  • Reflex: reflexive
  • Tense: tense
  • VerbForm: form of verb or deverbative
  • Voice: voice

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.