Giter Site home page Giter Site logo

matplotlib-pyodide's Introduction

NPM Latest Release PyPI Latest Release Build Status Documentation Status

Pyodide is a Python distribution for the browser and Node.js based on WebAssembly.

What is Pyodide?

Pyodide is a port of CPython to WebAssembly/Emscripten.

Pyodide makes it possible to install and run Python packages in the browser with micropip. Any pure Python package with a wheel available on PyPi is supported. Many packages with C extensions have also been ported for use with Pyodide. These include many general-purpose packages such as regex, PyYAML, lxml and scientific Python packages including NumPy, pandas, SciPy, Matplotlib, and scikit-learn.

Pyodide comes with a robust Javascript ⟺ Python foreign function interface so that you can freely mix these two languages in your code with minimal friction. This includes full support for error handling, async/await, and much more.

When used inside a browser, Python has full access to the Web APIs.

Try Pyodide (no installation needed)

Try Pyodide in a REPL directly in your browser. For further information, see the documentation.

Getting Started

Pyodide offers three different ways to get started depending on your needs and technical resources. These include:

  • Use a hosted distribution of Pyodide: see the Getting Started documentation.
  • Download a version of Pyodide from the releases page and serve it with a web server.
  • Build Pyodide from source
    • Build natively with make: primarily for Linux users who want to experiment or contribute back to the project.
    • Use a Docker image: recommended for Windows and macOS users and for Linux users who prefer a Debian-based Docker image with the dependencies already installed.

History

Pyodide was created in 2018 by Michael Droettboom at Mozilla as part of the Iodide project. Iodide is an experimental web-based notebook environment for literate scientific computing and communication.

Iodide is no longer maintained. If you want to use Pyodide in an interactive client-side notebook, see Pyodide notebook environments.

Contributing

Please view the contributing guide for tips on filing issues, making changes, and submitting pull requests. Pyodide is an independent and community-driven open-source project. The decision-making process is outlined in the Project governance.

Communication

License

Pyodide uses the Mozilla Public License Version 2.0.

matplotlib-pyodide's People

Contributors

kmdupr33 avatar maxg203 avatar pre-commit-ci[bot] avatar rth avatar ryanking13 avatar yu0a 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

matplotlib-pyodide's Issues

Matplotlib output using pyodide.runPython()

I hope this is the right place to put this - how do I access the plots made by the matplotlib package? I am using the pyodide package independent of iodide and I don't know how to use the plots made by matplotlib. I have a similar problem to pyodide/pyodide#365 but it never received a response. Thanks in advance!

plotting fails with `document.pyodideMplTarget` set

I attempted to use document.pyodideMplTarget to set a target element for a plot, and obtained this error message:

  File "<exec>", line 59, in tplot
  File "/lib/python3.11/site-packages/matplotlib/pyplot.py", line 389, in show
    return _get_backend_mod().show(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.11/site-packages/matplotlib_pyodide/html5_canvas_backend.py", line 450, in show
    plt.gcf().canvas.show()
  File "/lib/python3.11/site-packages/matplotlib_pyodide/browser_backend.py", line 117, in show
    div = self._create_root_element()
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.11/site-packages/matplotlib_pyodide/browser_backend.py", line 317, in _create_root_element
    mpl_target.appendChild(div)
    ^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'appendChild'
    M https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    new_error https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    _pythonexc2js https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    callPyObjectKwargs https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    callPyObject https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    apply https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    apply https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.asm.js:9
    replot http://localhost:8000/lumped_capacitance.html:98
    async* http://localhost:8000/lumped_capacitance.html:103
    EventListener.handleEvent* http://localhost:8000/lumped_capacitance.html:102

I've attached my html code for this error. For now, I'm just using the default append behavior.
lumped_capacitance.html.txt

wasm_backend vs. html5_canvas_backend

Does anyone know what the difference between wasm_backend vs. html5_canvas_backend?

  • The readme doesn't say
  • Google doesn't say anything
  • Nothing on stackoverflow.
  • None of old issues explain it
  • Looking at the source, there's not comments on html5_canvas_backend as to why it exists, and the documentation for wasm_backend sounds like it can apply to both.

I am using html5_canvas_backend with pyodide, which runs on web-assembly. So then what is wasm_backend? Isn't all of this wasm?

Yes, I would like answer to this question, but more than that, how is someone new to this project supposed to figure out what the readme means by these two bullets:

  • for the wasm backend,
  • for the interactive HTML5 backend;

I feel like there's some document or wiki missing. Or perhaps, there's an entirely different git repository somewhere that has all this information.

Any info at all how to navigate this space would be greatly appreciated.

Document some pyodide matplotlib suggestions for node and deno

The Node and Deno contexts can't use the matplotlib_pyodide backends but work fine with Agg.

Takes a second to piece together but one pattern I'm finding useful is to save the output as a data: url.

import base64
import io 
import numpy as np
import matplotlib
from matplotlib import pyplot as plt

matplotlib.use('Agg')

x = np.linspace(0, 2 * np.pi, 200)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y)

pic_IObytes = io.BytesIO()
plt.savefig(pic_IObytes, format='png')
pic_IObytes.seek(0)
pic_hash = base64.b64encode(pic_IObytes.read()).decode('utf-8')
dataurl = f'data:image/png;base64,{pic_hash}'
dataurl

Which seems to work! :D

Couldn't think of the right place for it but I think an example like that could be handy (even if just in this issue). Also interested in easier / more efficient patterns for sharing matplotlib figures between the python and JS contexts.

AttributeError: 'TimerWasm' object has no attribute '_timer'

Problem

Another community member and I were trying to see if Matplotlib's animation.FuncAnimation would work in PyScript using this Matplotlib demo.

We ran into the following error:

AttributeError: 'TimerWasm' object has no attribute '_timer'

The code is available here on PyScript.com.

More Details

Here's the stack trace of the error:

Traceback (most recent call last):
  File "/lib/python311.zip/_pyodide/_base.py", line 499, in eval_code
    .run(globals, locals)
     ^^^^^^^^^^^^^^^^^^^^
  File "/lib/python311.zip/_pyodide/_base.py", line 340, in run
    coroutine = eval(self.code, globals, locals)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<exec>", line 78, in <module>
  File "/lib/python3.11/site-packages/matplotlib/animation.py", line 1634, in __init__
    super().__init__(fig, **kwargs)
  File "/lib/python3.11/site-packages/matplotlib/animation.py", line 1395, in __init__
    event_source = fig.canvas.new_timer(interval=self._interval)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.11/site-packages/matplotlib_pyodide/browser_backend.py", line 416, in new_timer
    return TimerWasm(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.11/site-packages/matplotlib/backend_bases.py", line 1097, in __init__
    self.interval = 1000 if interval is None else interval
    ^^^^^^^^^^^^^
  File "/lib/python3.11/site-packages/matplotlib/backend_bases.py", line 1139, in interval
    self._timer_set_interval()
  File "/lib/python3.11/site-packages/matplotlib_pyodide/browser_backend.py", line 510, in _timer_set_interval
    if self._timer is not None:
       ^^^^^^^^^^^
AttributeError: 'TimerWasm' object has no attribute '_timer'

META: matplotlib issues

  • Interactivity -- need to write a new HTML Canvas-based backend
  • tri module -- skipped for now due to some compilation issues
  • qhull module -- skipped for now due to some compilation issues
  • custom fonts -- for now, it only has access to the "shipped" fonts
  • HiDPI (retina) support -- it currently displays a low-rez image
  • Reduce size -- clean out unnecessary stuff, like GUI backends, external tex support

Attempts to grab fonts from `${origin}/fonts`?

Using this example that's linked from the official blog post:

https://jsfiddle.net/gh/get/library/pure/pyodide/pyodide-blog/contents/demos/canvas-renderer-matplotlib/demo-1/

It tries and fails to get https://fiddle.jshell.net/fonts/DejaVuSans.ttf. I tried updating it to the lastest version by replacing the script tag with:

<script src="https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js"></script>

and swapping this:

matplotlib.use("module://matplotlib.backends.html5_canvas_backend")

for the newer:

matplotlib.use("module://matplotlib_pyodide.html5_canvas_backend")

But the same issue occurs. I'm guessing this is why the pan/zoom buttons of plots aren't the right size, and don't have any text in them, in the example linked above? (and also in the latest version of Pyodide)

Add support for plt.pause(...)

Matplotlib's plt.pause(...) causes the below error. It will error seemingly because it calls plt.show(block=False), whereas this repository does not appear to support calls to plt.show with pass-through kwargs.

Full stacktrace:

Uncaught (in promise) PythonError: Traceback (most recent call last):
  File "/lib/python311.zip/\_pyodide/\_base.py", line 571, in eval\_code\_async
    await CodeRunner(
  File "/lib/python311.zip/\_pyodide/\_base.py", line 394, in run\_async
    coroutine = eval(self.code, globals, locals)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\<exec>", line 113, in \<module>
  File "\<exec>", line 101, in main
  File "/lib/python3.11/site-packages/matplotlib/pyplot.py", line 568, in pause
    show(block=False)
  File "/lib/python3.11/site-packages/matplotlib/pyplot.py", line 389, in show
    return \_get\_backend\_mod().show(\*args, \*\*kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: \_BackendWasmCoreAgg.show() got an unexpected keyword argument 'block'

Matplotlib backend in a web worker

At the moment using matplotlib when Pyodide is loaded in a web worker with the following code snippet:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 10, 1000)
plt.plot(x, np.sin(x));

plt.show()

Gives the following error as it tries to create new elements with the wasm backend:

Traceback (most recent call last):
  File "<console>", line 2, in <module>
  File "/lib/python3.8/site-packages/matplotlib/pyplot.py", line 2336, in <module>
    switch_backend(rcParams["backend"])
  File "/lib/python3.8/site-packages/matplotlib/pyplot.py", line 276, in switch_backend
    class backend_mod(matplotlib.backend_bases._Backend):
  File "/lib/python3.8/site-packages/matplotlib/pyplot.py", line 277, in backend_mod
    locals().update(vars(importlib.import_module(backend_name)))
  File "/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/lib/python3.8/site-packages/matplotlib/backends/wasm_backend.py", line 23, in <module>
    from js import document
ImportError: cannot import name 'document' from 'js' (unknown location)

There is already a "Caveats" section in the docs mentioning limitations when using Pyodide in a web worker: https://pyodide.org/en/latest/usage/webworker.html#caveats

Maybe there could be another backend for matplotlib that would work in a web worker. Or document a workaround (if that's possible).

matplotlab chinese font messy code

πŸ› Bug

I am using Pyodide to run matplotlab pie with chinese font label, but display nothing.

To Reproduce

import matplotlib.pyplot as plt
kinds = ["δΈ­ζ–‡1", "δΈ­ζ–‡2", "δΈ­ζ–‡3","δΈ­ζ–‡4"]
percentage = [0.0881, 0.3964, 0.4955, 0.0198 ]
plt.figure(figsize=(10,8))
plt.pie(x=percentage, labels=kinds, textprops= {"fontsize":13, "color":"w"})

plt.rcParams[ 'font.family' ] = [ 'Microsoft YaHei' ]
plt.rcParams[ 'axes.unicode_minus' ] = False
plt.title("δΈ­ζ–‡4", fontsize=13, color="w")
plt.legend(loc=4, fontsize=12)
plt.show()
plt

Expected behavior

in windows and mac python runtime, we need to find the right font to display the right font

Environment

  • Pyodide Version:0.21.2
  • Browser version:109.0.5414.120
  • Any other relevant information: call from pyscript

matplotlib always generates double times of output divs

πŸ› Bug

Use Pyodide and matplotlib to output 1 picture, the website will show 2 pictures; if 2, then 4, etc...

To Reproduce

Clone this repo

Expected behavior

the correct amount of picture output

Environment

  • Pyodide Version:0.24.1
  • Browser version:Chrome 120.0.6099.227
  • Any other relevant information:

Additional context

matplotlib: savefig() produces TypeError: Invalid argument type in ToBigInt operation

Hello fellow pyodide users and mods!! Thank you for your hard work, this is an awesome project and I've been using it heavily!! Most recently, I've been trying to plot clustermaps from seaborn. This works amazingly on Chrome and somewhat well on Firefox, but not at all on Safari. This is the error that I've been getting when I call savefig() from the matplotlib library:

Screen Shot 2021-11-02 at 2 22 07 AM

Chasing down the most recent stack call, it's this function:
Screen Shot 2021-11-02 at 2 23 26 AM

Before I go even further down the rabbit hole to bisect this error, has anyone already encountered this and know how to get around it? Is this even the right place to ask this question? This is my first experience with cross-platform software, so any advice would be greatly appreciated. Thanks in advance, and hope y'all have a lovely day!!

Interactive Slider Issue: 'Figure' object has no attribute 'canvas'

The following code is taken directly from matplotlib website https://matplotlib.org/stable/gallery/widgets/slider_demo.html. It generates red errors 'Figure' object has no attribute 'canvas'. Even though the plot itself work as expected! I tried to put everything in a try block, didn't help. Is there anyway the catch this error?

Uncaught PythonError: Traceback (most recent call last):
  File "/lib/python3.11/site-packages/matplotlib_pyodide/browser_backend.py", line 221, in onmousemove
    self.motion_notify_event(x, y, guiEvent=event)
  File "/lib/python3.11/site-packages/matplotlib/backend_bases.py", line 1926, in motion_notify_event
    self.callbacks.process(s, event)
  File "/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 292, in process
    self.exception_handler(exc)
  File "/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 96, in _exception_printer
    raise exc
  File "/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 287, in process
    func(*args, **kwargs)
  File "/lib/python3.11/site-packages/matplotlib/widgets.py", line 534, in _update
    self.set_val(val)
  File "/lib/python3.11/site-packages/matplotlib/widgets.py", line 568, in set_val
    self._observers.process('changed', val)
  File "/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 292, in process
    self.exception_handler(exc)
  File "/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 96, in _exception_printer
    raise exc
  File "/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 287, in process
    func(*args, **kwargs)
  File "/lib/python3.11/site-packages/matplotlib/widgets.py", line 585, in <lambda>
    return self._observers.connect('changed', lambda val: func(val))
                                                          ^^^^^^^^^
  File "<exec>", line 92, in update
AttributeError: 'Figure' object has no attribute 'canvas'
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.widgets import Button, Slider


# The parametrized function to be plotted
def f(t, amplitude, frequency):
    return amplitude * np.sin(2 * np.pi * frequency * t)

t = np.linspace(0, 1, 1000)

# Define initial parameters
init_amplitude = 5
init_frequency = 3

# Create the figure and the line that we will manipulate
fig, ax = plt.subplots()
line, = ax.plot(t, f(t, init_amplitude, init_frequency), lw=2)
ax.set_xlabel('Time [s]')

# adjust the main plot to make room for the sliders
fig.subplots_adjust(left=0.25, bottom=0.25)

# Make a horizontal slider to control the frequency.
axfreq = fig.add_axes([0.25, 0.1, 0.65, 0.03])
freq_slider = Slider(
    ax=axfreq,
    label='Frequency [Hz]',
    valmin=0.1,
    valmax=30,
    valinit=init_frequency,
)

# Make a vertically oriented slider to control the amplitude
axamp = fig.add_axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
    ax=axamp,
    label="Amplitude",
    valmin=0,
    valmax=10,
    valinit=init_amplitude,
    orientation="vertical"
)


# The function to be called anytime a slider's value changes
def update(val):
    line.set_ydata(f(t, amp_slider.val, freq_slider.val))
    fig.canvas.draw_idle()


# register the update function with each slider
freq_slider.on_changed(update)
amp_slider.on_changed(update)

# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.
resetax = fig.add_axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, 'Reset', hovercolor='0.975')


def reset(event):
    freq_slider.reset()
    amp_slider.reset()
button.on_clicked(reset)

plt.show()

Custom fonts

From #9

for now, matplotlib only has access to the "shipped" fonts

Dynamically updating plots does not appear to play well with plt.figure

Hi all,

Please forgive me if I'm doing something wrong, but I've been struggling to find any sort of documentation about how to functionally replace iPython widgets and the only Python thing I've found has been this notebook. As a non-Javascript user the Lorenz notebook is over my head.

I'm working on a notebook here where I want to be able to have the user give a number, and which serves as a random seed and gives a bingo card for this. Following the example from the log-normal notebook, I can get things to refresh, but if I add a line to define my figsize plt.figure(figsize=(5,5), dpi=300) after plt.cla(), I get tons of garbage empty plots after whenever I update the value followed by the correct plot.

The default plot size is too small for what I want, so I want to see if there is a better way to go about making these plots and sizing them properly.

Thanks!
Paul

setting plt.rcParams['figure.figsize'] does not work.

I tried to make matplotlib responsive by doing this little trick

from js import window 
plt.rcParams['figure.figsize'] = [window.innerWidth*window.devicePixelRatio, 6] 
plt.rcParams["figure.dpi"] = window.devicePixelRatio

However, it does not work. Interestingly, even if I hardcode figsize, it still does not respect my parameters. What should be done? (the figure is interactive. i.e it has a slider)

plt.figure() throws if not immediately followed by more plotting

I need this to set up the report markup (ideally I'd just have <figure id="somematplotlibfigureid" /> in html and then fill them up later) and do the plotting later.

%% py
import numpy as np
import matplotlib.pyplot as plt

%% py
plt.figure('figfoo')
plt.plot(np.arange(10), np.arange(10))
plt.show()
# works

%% py
plt.figure('figbar')
# throws
# <Figure size 640x480 with 0 Axes>
# A value renderer encountered an error.
# TypeError: r.propertyIsEnumerable is not a function
#
#    in t
#    in t
#    in t
#    in t
#    in v
#    in div
#    in Styled(div)
#    in he
#    in qt
#    in div
#    in Styled(div)
#    in div
#    in Styled(div)
#    in bn
#    in jn
#    in p
#    in div
#    in div
#    in div
#    in ir
#    in p
#    in div
#    in u
#    in p
#    in lr
#    in p
#    in t
#    in p
# Please file a bug report.

a better method for configuring where matplotlib figures are added to the DOM

Currently, matplotlib figures are placed in an element returned by FigureCanvasWasm.create_root_element. Our options for modifying this behavior are limited, and I'd like to propose something better.

Current options

@personalizedrefrigerator, proposed here that we can build pyodide ourselves and subclass FigureCanvasWasm to get the behavior we want.

@bgailleton demonstrated a monkey-patch-based approach here where he modifies create_root_element on a Canvas instance. (@gzuidhof does something similar here in starboard-notebook)

A better approach?

FigureCanvasWasm.create_root_element can return js.document.pyodideMatplotlibPlotTarget if it refers to a DOM element and fallback on its default behavior of creating an unattached div if that element isn't present. The code would look something like this:

def create_root_element(self):
  if document.pyodideMatplotlibPlotTarget:
    return document.pyodideMatplotlibPlotTarget
  else:
    return document.createElement("div")

With this in place, clients could control where plots are rendered simply by populating document.pyodideMatplotlibPlotTarget. I think this is much easier than doing a custom build of pyodide and much cleaner than monkey-patching instances. If we did this, we also wouldn't need to have subclasses override the method. They could control where plots are rendered in the same way that external clients do. I'm happy to contribute a PR if we think this is a good approach.

"module://matplotlib_pyodide.wasm_backend" should auto clear the old pictures instead of generate new pictures

πŸ› Bug

When use "module://matplotlib_pyodide.wasm_backend" to output pictures, each time it appends new divs and let the height of the page changes. That's unacceptable for admin apps.

To Reproduce

Clone this repo

  • deploy and run the code
  • go to the page http://localhost:9000/#/matplotlib
  • click the northwest button "ζ‰§θ‘Œ" >= 2 times and you will see the page become longer and longer

Expected behavior

the constant amount of picture output

Environment

  • Pyodide Version:0.24.1
  • Browser version:Chrome 120.0.6099.227
  • Any other relevant information:

Additional context

Matplotlib does not work

πŸ› Bug

Matplotlib does not render plots

To Reproduce

  1. go to https://pyodide.org/en/latest/console.html
  2. run the following:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> plt.plot(np.sin(np.linspace(0, 0.05, 200) * np.pi * 2 * 100))
[<matplotlib.lines.Line2D object at 0x1fc7878>]
>>> plt.show()
>>>

Expected behavior

The plot shows up, either inline or as popup

Environment

  • Pyodide Version: latest
  • Browser version: Google Chrome, 96.0.4664.93Β (Official Build)Β (64-bit)

Use matplotlib figures externally

Hey there,
Is it possible to generate the figures including first and then use them on a static webpage without any python or matplotlib?
Including hovering over the points and zooming in?
I tried it already, I think the problem here is that the data is stored in events:
image

Thanks
Markus

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.