Giter Club home page Giter Club logo

jack-in-the-box's People

Contributors

hark130 avatar

Stargazers

 avatar

Watchers

 avatar  avatar

jack-in-the-box's Issues

JITB-20: Refactor Quiplash 3 support to use new modular functionality

Refactor JbgQ3 to use code from jitb_selenium. Also, write unit tests for the legacy Quiplash 3 functionality for regression testing. Finally, probably entirely reorganize JbgQ3 to look more like JbgQ2 because I suspect JbgQ2 is an improvement.

These two tasks from the running To Do list in #9 commit messages got left on the table:

    [ ] Refactor JbgQ3 to use jitb_selenium (and friends)
    [ ] Write JbgQ3 unit tests

JITB-17: Fix "missing Chrome" BUG

I was running JITB in Windows. No Chrome installed. It hung. Couldn't kill it. No error message(?).

C:\Users\hark1\Documents\Personal\Programming\jack-in-the-box>python -m jitb --room klaf --user jitb --debug
Traceback (most recent call last):
  File "C:\Program Files\Python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Program Files\Python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\hark1\Documents\Personal\Programming\jack-in-the-box\jitb\__main__.py", line 10, in <module>
    sys.exit(main())
  File "C:\Users\hark1\Documents\Personal\Programming\jack-in-the-box\jitb\jitb_main.py", line 29, in main
    play_the_game(room_code=room_code, username=username, ai_obj=client)
  File "C:\Users\hark1\Documents\Personal\Programming\jack-in-the-box\jitb\jitb_website.py", line 106, in play_the_game
    web_driver = join_room(room_code=room_code, username=username)  # Webdriver for Jackbox Games
  File "C:\Users\hark1\Documents\Personal\Programming\jack-in-the-box\jitb\jitb_website.py", line 81, in join_room
    driver = webdriver.Chrome()  # Webdriver object
  File "C:\Users\hark1\AppData\Roaming\Python\Python39\site-packages\selenium\webdriver\chrome\webdriver.py", line 45, in __init__
    super().__init__(
  File "C:\Users\hark1\AppData\Roaming\Python\Python39\site-packages\selenium\webdriver\chromium\webdriver.py", line 61, in __init__
    super().__init__(command_executor=executor, options=options)
  File "C:\Users\hark1\AppData\Roaming\Python\Python39\site-packages\selenium\webdriver\remote\webdriver.py", line 208, in __init__
    self.start_session(capabilities)
  File "C:\Users\hark1\AppData\Roaming\Python\Python39\site-packages\selenium\webdriver\remote\webdriver.py", line 292, in start_session
    response = self.execute(Command.NEW_SESSION, caps)["value"]
  File "C:\Users\hark1\AppData\Roaming\Python\Python39\site-packages\selenium\webdriver\remote\webdriver.py", line 347, in execute
    self.error_handler.check_response(response)
  File "C:\Users\hark1\AppData\Roaming\Python\Python39\site-packages\selenium\webdriver\remote\errorhandler.py", line 229, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: DevToolsActivePort file doesn't exist
Stacktrace:
        GetHandleVerifier [0x00007FF758AD7012+3522402]
        (No symbol) [0x00007FF7586F8352]
        (No symbol) [0x00007FF7585A5ABB]
        (No symbol) [0x00007FF7585DB0FA]
        (No symbol) [0x00007FF7585D4E3E]
        (No symbol) [0x00007FF7585D2188]
        (No symbol) [0x00007FF758619469]
        (No symbol) [0x00007FF75860EE03]
        (No symbol) [0x00007FF7585DF4D4]
        (No symbol) [0x00007FF7585E05F1]
        GetHandleVerifier [0x00007FF758B09B9D+3730157]
        GetHandleVerifier [0x00007FF758B5F02D+4079485]
        GetHandleVerifier [0x00007FF758B575D3+4048163]
        GetHandleVerifier [0x00007FF75882A649+718233]
        (No symbol) [0x00007FF758704A3F]
        (No symbol) [0x00007FF7586FFA94]
        (No symbol) [0x00007FF7586FFBC2]
        (No symbol) [0x00007FF7586EF2E4]
        BaseThreadInitThunk [0x00007FFF2D487344+20]
        RtlUserThreadStart [0x00007FFF2E2C26B1+33]

JITB-27: Does the debug logger have a BUG?

Running the unit tests resulted in some unexpected failures (on JITB-13). Is this my problem? (Top of my head... am I not closing the debug log file?!) Is this the OS's problem? (I've got the --debug flag turned on for all unit tests, even though I haven't used any of them in a while) If it's the OS's problem, I still need to work around it. Maybe I default all of these test clases to debug = False but turn on unit test debugging, as needed.

SIDE NOTE: I've had some success in the past passing a command-line argument to the python -m test command... Maybe that's the way to turn on unit test debugging.

======================================================================
ERROR: test_s07_round_3_prompt_1_v2 (unit_test.test_selenium.test_get_web_element_text.SpecialTestJitbSeleniumGetWebElementText)
KEY TEXT: Quiplash 2 Round 3 Last Lash prompt page v2; value state-answer-question.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/test/unit_test/test_jackbox_games.py", line 56, in setUp
    Logger.initialize(debugging=True)  # Enable DEBUG logging while testing
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_logger.py", line 88, in initialize
  File "/usr/lib/python3.10/logging/__init__.py", line 1169, in __init__
  File "/usr/lib/python3.10/logging/__init__.py", line 1201, in _open
OSError: [Errno 24] Too many open files: '/tmp/jitb_20240305_124256.log'

======================================================================
ERROR: test_s08_round_3_prompt_1_v2_take_2 (unit_test.test_selenium.test_get_web_element_text.SpecialTestJitbSeleniumGetWebElementText)
KEY TEXT: Quiplash 2 Round 3 Last Lash prompt page v2; value question-text-alt.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/test/unit_test/test_jackbox_games.py", line 56, in setUp
    Logger.initialize(debugging=True)  # Enable DEBUG logging while testing
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_logger.py", line 88, in initialize
  File "/usr/lib/python3.10/logging/__init__.py", line 1169, in __init__
  File "/usr/lib/python3.10/logging/__init__.py", line 1201, in _open
OSError: [Errno 24] Too many open files: '/tmp/jitb_20240305_124256.log'

======================================================================
ERROR: test_s09_round_3_vote_1_v1 (unit_test.test_selenium.test_get_web_element_text.SpecialTestJitbSeleniumGetWebElementText)
KEY TEXT: Quiplash 2 Round 3 Last Lash vote page v1; value state-vote.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/test/unit_test/test_jackbox_games.py", line 56, in setUp
    Logger.initialize(debugging=True)  # Enable DEBUG logging while testing
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_logger.py", line 88, in initialize
  File "/usr/lib/python3.10/logging/__init__.py", line 1169, in __init__
  File "/usr/lib/python3.10/logging/__init__.py", line 1201, in _open
OSError: [Errno 24] Too many open files: '/tmp/jitb_20240305_124256.log'

======================================================================
ERROR: test_s11_round_3_vote_1_v2 (unit_test.test_selenium.test_get_web_element_text.SpecialTestJitbSeleniumGetWebElementText)
KEY TEXT: Quiplash 2 Round 3 Last Lash vote page v2; value state-vote.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/test/unit_test/test_jackbox_games.py", line 56, in setUp
    Logger.initialize(debugging=True)  # Enable DEBUG logging while testing
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_logger.py", line 88, in initialize
  File "/usr/lib/python3.10/logging/__init__.py", line 1169, in __init__
  File "/usr/lib/python3.10/logging/__init__.py", line 1201, in _open
OSError: [Errno 24] Too many open files: '/tmp/jitb_20240305_124256.log'

======================================================================
ERROR: test_s13_non_jackbox_game_page (unit_test.test_selenium.test_get_web_element_text.SpecialTestJitbSeleniumGetWebElementText)
Non-Jackbox Games website.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/test/unit_test/test_jackbox_games.py", line 56, in setUp
    Logger.initialize(debugging=True)  # Enable DEBUG logging while testing
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_logger.py", line 88, in initialize
  File "/usr/lib/python3.10/logging/__init__.py", line 1169, in __init__
  File "/usr/lib/python3.10/logging/__init__.py", line 1201, in _open
OSError: [Errno 24] Too many open files: '/tmp/jitb_20240305_124256.log'

JITB-11: Try to get better performance from OpenAI with better prompts

Sometimes, the AI is playing a different game than the humans: gross, straight, hilarious, raunchy.

Fiddle with the prompt so that OpenAI's responses match the tone and style of the human answers it observes.
Also, ensure we're using an API capable of keeping a running history.

JITB-23: Let the AI participate in the audience

Should be easy enough.
Add auto-support for the "audience" button, in addition to the join button, and then parse the pages as normal.
Legacy logic probably won't care if it doesn't see a prompt page.

JITB-21: Odd silent crash/timeout with Quiplash 3

Played legacy code against an online game of Quiplash 3.
Python hung.
Log was empty except for the room validation.
Got this when I sent a KeyboardInterrupt...

Traceback (most recent call last):
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_website.py", line 71, in play_the_game
    jbg_obj.play(web_driver=web_driver)
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jbgames/jbg_q3.py", line 33, in play
    self.validate_status(web_driver=web_driver)
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jbgames/jbg_q3.py", line 178, in validate_status
    self._check_web_driver(web_driver=web_driver)
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jbgames/jbg_abc.py", line 128, in _check_web_driver
    temp_we = web_driver.find_element(By.ID, 'swal2-title')
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 742, in find_element
    return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 346, in execute
    response = self.command_executor.execute(driver_command, params)
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py", line 300, in execute
    return self._request(command_info[0], url, body=data)
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py", line 321, in _request
    response = self._conn.request(method, url, body=body, headers=headers)
  File "/usr/lib/python3/dist-packages/urllib3/request.py", line 78, in request
    return self.request_encode_body(
  File "/usr/lib/python3/dist-packages/urllib3/request.py", line 170, in request_encode_body
    return self.urlopen(method, url, **extra_kw)
  File "/usr/lib/python3/dist-packages/urllib3/poolmanager.py", line 375, in urlopen
    response = conn.urlopen(method, u.request_uri, **kw)
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 700, in urlopen
    httplib_response = self._make_request(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 446, in _make_request
    six.raise_from(e, None)
  File "<string>", line 3, in raise_from
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 441, in _make_request
    httplib_response = conn.getresponse()
  File "/usr/lib/python3.10/http/client.py", line 1375, in getresponse
    response.begin()
  File "/usr/lib/python3.10/http/client.py", line 318, in begin
    version, status, reason = self._read_status()
  File "/usr/lib/python3.10/http/client.py", line 279, in _read_status
    line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
  File "/usr/lib/python3.10/socket.py", line 705, in readinto
    return self._sock.recv_into(b)
KeyboardInterrupt

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 169, in _new_conn
    conn = connection.create_connection(
  File "/usr/lib/python3/dist-packages/urllib3/util/connection.py", line 96, in create_connection
    raise err
  File "/usr/lib/python3/dist-packages/urllib3/util/connection.py", line 86, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [Errno 111] Connection refused

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 700, in urlopen
    httplib_response = self._make_request(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 395, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 234, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "/usr/lib/python3.10/http/client.py", line 1283, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1329, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1278, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1038, in _send_output
    self.send(msg)
  File "/usr/lib/python3.10/http/client.py", line 976, in send
    self.connect()
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 200, in connect
    conn = self._new_conn()
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 181, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x7a4dc80a7760>: Failed to establish a new connection: [Errno 111] Connection refused

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/__main__.py", line 10, in <module>
    sys.exit(main())
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_main.py", line 29, in main
    play_the_game(room_code=room_code, username=username, ai_obj=client)
  File "/home/joe/Documents/Personal/Programming/jack-in-the-box/jitb/jitb_website.py", line 75, in play_the_game
    web_driver.close()
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 459, in close
    self.execute(Command.CLOSE)
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 346, in execute
    response = self.command_executor.execute(driver_command, params)
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py", line 300, in execute
    return self._request(command_info[0], url, body=data)
  File "/home/joe/.local/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py", line 321, in _request
    response = self._conn.request(method, url, body=body, headers=headers)
  File "/usr/lib/python3/dist-packages/urllib3/request.py", line 74, in request
    return self.request_encode_url(
  File "/usr/lib/python3/dist-packages/urllib3/request.py", line 96, in request_encode_url
    return self.urlopen(method, url, **extra_kw)
  File "/usr/lib/python3/dist-packages/urllib3/poolmanager.py", line 375, in urlopen
    response = conn.urlopen(method, u.request_uri, **kw)
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 784, in urlopen
    return self.urlopen(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 784, in urlopen
    return self.urlopen(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 784, in urlopen
    return self.urlopen(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 756, in urlopen
    retries = retries.increment(
  File "/usr/lib/python3/dist-packages/urllib3/util/retry.py", line 574, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', port=46065): Max retries exceeded with url: /session/091cdcba9bb55067743d4d884c41c2c7/window (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7a4dc80a7760>: Failed to establish a new connection: [Errno 111] Connection refused'))

jitb_20240229_231938.log

JITB-25: Formalize releases

I should likely be merging dev into main every time I complete support for a new Jackbox game (for now).
I should also be updating the repo README.md (because it's no longer entirely accurate).
Even better, I should devise a release checklist.
Even better still... what happened to my "release jitb as a wheel" backlog ticket. (This might be it, now)
I should start a CHANGELOG as well.
Don't forget about jitb dependencies.

Acceptance Criteria:

  • There's a formal release checklist on the wiki
  • README updated
  • CHANGELOG started
  • devops code added to automate the release of jitb as a wheel
  • Dependencies file added to the project
  • Top-level package __init__.py contains adequate usage instructions

JITB-6: Try to improve Thriplash responses

Manual testing isn't getting very far on #1 so I think the plan is:

  1. Work around the issue on #1 (by detecting, parsing, and choosing a sub-selection)
  2. Try to dedicate effort to finding a better way to prompt for Thriplashes:
  • Swap back to the chat.completion endpoint and try to use assistant content, in the messages, to clue the AI in on how best to answer those types of questions (Downside... additional tokens wasted)
  • Tweak the prompt
  • Ask for clarification when the AI gets it wrong.

SIDE NOTE: Manual testing on #1 looked like this:

import time
from jitb.jitb_openai import JitbAi
test = JitbAi()

test_cases = ['After the first seven, what are the next three deadly sins?', 'Three ways to take your next book club meeting up a notch', 'Three things that are considered bad etiquette at the gym', 'Three words you don’t want to hear someone use to describe your genitals', 'When going through airport security, make sure to remove your _______, _______, and _______', 'Three Chevy car models recalled for being “too awesome”', 'The three things you need to be good at to truly get ahead in life', 'Bad news: they now take away your “cool card” if you like _______, _______, or _______', 'You know you’re about to have a bad day on Twitter if you see these three hashtags', 'Three things Mars needs before we can live there', 'Three theme parks more dangerous than Jurassic Park']

test.generate_thriplash(prompt=test_cases[0])

for test_case in test_cases:
    time.sleep(20)
    test.generate_thriplash(prompt=test_case)

Output was:

['Snoring', 'Procrastination', 'Overindulgence']
['Booze and books!', 'Add a theme.', 'Bring snacks.']
['Grunting loudly', 'Not wiping sweat', 'Hogging equipment']
OpenAI generated more than just three lines here ['1. Small, shriveled, sad', '2. Moldy, crusty, stinky', '3. Flaccid, limp, lifeless', '4. Withered, wrinkly, dry', '5. Tiny, puny']
['Flaccid, limp, lifeless', 'Withered, wrinkly, dry', 'Tiny, puny']
['shoes', 'belt', 'liquids']
['Camaro', 'Corvette', 'Impala']
OpenAI generated more than just three lines here ['1. Lying, cheating, stealing', '2. Kissing, ass, networking', '3. Hustling, grinding, scheming', '4. Multitasking, bullshitting, bribing', '5. Manipulating, schmoo']
['Hustling, grinding, scheming', 'Multitasking, bullshitting, br', 'Manipulating, schmoo']
['- Nickelback', '- Crocs', '- Pineapple on pizza']
['#CancelCulture', '#TwitterFingers', '#TwitterMeltdown']
['Oxygen', 'Water', 'WiFi']
['Clown Carnage Land', 'Zombie Apocalypse World', 'T-Rex Terror Town']

JITB-7: Support for legacy Quiplash

Couldn't be that hard to determine the interfaces for older versions of Quiplash and then implement them in a class, lookup table, mapping, etc.

Related to #9

Round 3 for Quiplash (legacy) is basically vote for your favorites. I think you can vote more than once for the same answer.

JITB-4: Better DEBUG logging

Currently, it just logs to stdout(?).
It would be best if it could log to a timestamped log file in a predictable way.

Blocks #2

JITB-3: Validate "Game has started" at login page

When JITB dies, a common human response would be to restart JITB with the same command line arguments.
However, this puts JITB into Audience mode rather than recovering (missing cookies?)... which may be ok... but there should be a warning. That, or maybe a --spectate mode to permit audience participation.

JITB-18: Replace the AI's username with a pronoun

Jackbox likes to throw game-playing usernames into dynamic prompts but the AI is unaware when a prompt is about it. Global find-and-replace the established --name value with an appropriate pronoun in all prompts: answer or vote.

JITB-22: Fix avatar selection race-condition with Quiplash 3

I encountered an issue with a large group playing Quiplash 3. (Which may actually be the cause for the #26 hang, now that I think about it)
The problem was that in between jitb selecting an avatar and clicking the avatar button, someone else already chose it. It resulted in no avatar selection.

Seems like there should be some way to verify that one of the buttons is selected. Basically, keep clicking avatars until one turns up "selected".

JITB-2: Validate the game at the login page

The login page actually shows you the game being played once you type in a room code.
Currently, there's only support for Quiplash3... and we wouldn't want JITB playing games it shouldn't be.

JITB-26: Allow JitbAi's caller to name the game being played

JitbAi is currently hard-coded to support Quiplash 3 even though Quiplash 2 support has already been implemented (and Joke Boat support is in progress).

Thinking about the current structure of the code, it might be best to allow the jitb_website functionality to instantiate the AI object instead of in main()... but you'll need to deconflict the teardown which was also happening in a finally block in main()

JITB-8: Support for Quiplash 2

Couldn't be that hard to determine the interfaces for older versions of Quiplash and then implement them in a class, lookup table, mapping, etc.

Related to #8
Blocked by #2

Round 3 for Quiplash 2 is _____.

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.