Giter Site home page Giter Site logo

readability's Introduction

Readability.js

A standalone version of the readability library used for Firefox Reader View.

Installation

Readability is available on npm:

npm install @mozilla/readability

You can then require() it, or for web-based projects, load the Readability.js script from your webpage.

Basic usage

To parse a document, you must create a new Readability object from a DOM document object, and then call the parse() method. Here's an example:

var article = new Readability(document).parse();

If you use Readability in a web browser, you will likely be able to use a document reference from elsewhere (e.g. fetched via XMLHttpRequest, in a same-origin <iframe> you have access to, etc.). In Node.js, you can use an external DOM library.

API Reference

new Readability(document, options)

The options object accepts a number of properties, all optional:

  • debug (boolean, default false): whether to enable logging.
  • maxElemsToParse (number, default 0 i.e. no limit): the maximum number of elements to parse.
  • nbTopCandidates (number, default 5): the number of top candidates to consider when analysing how tight the competition is among candidates.
  • charThreshold (number, default 500): the number of characters an article must have in order to return a result.
  • classesToPreserve (array): a set of classes to preserve on HTML elements when the keepClasses options is set to false.
  • keepClasses (boolean, default false): whether to preserve all classes on HTML elements. When set to false only classes specified in the classesToPreserve array are kept.
  • disableJSONLD (boolean, default false): when extracting page metadata, Readability gives precedence to Schema.org fields specified in the JSON-LD format. Set this option to true to skip JSON-LD parsing.
  • serializer (function, default el => el.innerHTML) controls how the content property returned by the parse() method is produced from the root DOM element. It may be useful to specify the serializer as the identity function (el => el) to obtain a DOM element instead of a string for content if you plan to process it further.
  • allowedVideoRegex (RegExp, default undefined ): a regular expression that matches video URLs that should be allowed to be included in the article content. If undefined, the default regex is applied.

parse()

Returns an object containing the following properties:

  • title: article title;
  • content: HTML string of processed article content;
  • textContent: text content of the article, with all the HTML tags removed;
  • length: length of an article, in characters;
  • excerpt: article description, or short excerpt from the content;
  • byline: author metadata;
  • dir: content direction;
  • siteName: name of the site;
  • lang: content language;
  • publishedTime: published time;

The parse() method works by modifying the DOM. This removes some elements in the web page, which may be undesirable. You can avoid this by passing the clone of the document object to the Readability constructor:

var documentClone = document.cloneNode(true);
var article = new Readability(documentClone).parse();

isProbablyReaderable(document, options)

A quick-and-dirty way of figuring out if it's plausible that the contents of a given document are suitable for processing with Readability. It is likely to produce both false positives and false negatives. The reason it exists is to avoid bogging down a time-sensitive process (like loading and showing the user a webpage) with the complex logic in the core of Readability. Improvements to its logic (while not deteriorating its performance) are very welcome.

The options object accepts a number of properties, all optional:

  • minContentLength (number, default 140): the minimum node content length used to decide if the document is readerable;
  • minScore (number, default 20): the minimum cumulated 'score' used to determine if the document is readerable;
  • visibilityChecker (function, default isNodeVisible): the function used to determine if a node is visible;

The function returns a boolean corresponding to whether or not we suspect Readability.parse() will succeed at returning an article object. Here's an example:

/*
    Only instantiate Readability  if we suspect
    the `parse()` method will produce a meaningful result.
*/
if (isProbablyReaderable(document)) {
    let article = new Readability(document).parse();
}

Node.js usage

Since Node.js does not come with its own DOM implementation, we rely on external libraries like jsdom. Here's an example using jsdom to obtain a DOM document object:

var { Readability } = require('@mozilla/readability');
var { JSDOM } = require('jsdom');
var doc = new JSDOM("<body>Look at this cat: <img src='./cat.jpg'></body>", {
  url: "https://www.example.com/the-page-i-got-the-source-from"
});
let reader = new Readability(doc.window.document);
let article = reader.parse();

Remember to pass the page's URI as the url option in the JSDOM constructor (as shown in the example above), so that Readability can convert relative URLs for images, hyperlinks, etc. to their absolute counterparts.

jsdom has the ability to run the scripts included in the HTML and fetch remote resources. For security reasons these are disabled by default, and we strongly recommend you keep them that way.

Security

If you're going to use Readability with untrusted input (whether in HTML or DOM form), we strongly recommend you use a sanitizer library like DOMPurify to avoid script injection when you use the output of Readability. We would also recommend using CSP to add further defense-in-depth restrictions to what you allow the resulting content to do. The Firefox integration of reader mode uses both of these techniques itself. Sanitizing unsafe content out of the input is explicitly not something we aim to do as part of Readability itself - there are other good sanitizer libraries out there, use them!

Contributing

Please see our Contributing document.

License

Copyright (c) 2010 Arc90 Inc

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

readability's People

Contributors

01abhishekjain avatar andrei-ch avatar andreskrey avatar ankushduacodes avatar da2x avatar danburzo avatar davidar avatar dependabot[bot] avatar dgellow avatar evanxd avatar gijsk avatar heycam avatar hsemarap avatar jakubriedl avatar leibovic avatar migueldoblado avatar mikesnare avatar mozilla-github-standards avatar n1k0 avatar palmeral avatar pdehaan avatar pmaoui avatar radhifadlillah avatar srlakhe avatar st3fan avatar tarekziade avatar tianzhich avatar tigt avatar treora avatar tthhtao avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

readability's Issues

Add library to npm and bower?

Not sure if we want to add these to npm and/or bower to make them easier to include in projects without copy/pasting code and then having an infinite versions of the library out in the wild.

Readabily fails at parsing pages using svg camel-cased tags

Sample test case:

<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" height="50" width="50" style="position: absolute;">
  <g>
    <clipPath id="hex-mask-large"><polygon points="15,35 10,35 10,0 10,0 45,0 45,35 45,35 25,35 15,43"/></clipPath>
    <clipPath id="hex-mask-small"><polygon points="5,1 5,16 3,23 10,20 24,20 24,1"/></clipPath>
  </g>
</svg>

Gives:

JSDOMParser error: expected '</clippath>'
JSDOMParser error: expected '</svg>'
JSDOMParser error: expected '</body>'
JSDOMParser error: expected '</html>'

Replacing clipPath with clippath "fixes" the issue. There's definitely an issue with force-lowercasing tag names here.

Add support for querySelectorAll() to JSDOMParser?

That would ease a bunch performing batch operations, and do them in a much more efficient manner, eg.

[].forEach.call(doc.querySelectorAll("object,embed,iframe"), function(x) {x.remove()})

I suspect this is a bunch of work :(

JSDOMParser should handle uppercase tag names

Sample test case:

describe("Tag name case handling", function() {
  it("should handle lowercase tag names", function() {
    var html = "<div>plop</div>";
    var doc = new JSDOMParser().parse(html);
    expect(doc.firstChild.tagName).eql("DIV");
    expect(doc.firstChild.localName).eql("div");
  });

  it("should handle uppercase tag names", function() {
    var html = "<DIV>plop</DIV>";
    var doc = new JSDOMParser().parse(html);
    expect(doc.firstChild.tagName).eql("DIV");
    expect(doc.firstChild.localName).eql("div");
  });
});

The first test passes but the second test fails:

JSDOMParser error: expected '</div>'

  1) Tag name case handling should handle uppercase tag names:
     TypeError: Cannot read property 'tagName' of null
      at Context.<anonymous> (/Users/niko/Sites/readability/test/test-jsdomparser.js:282:26)
      at callFn (/Users/niko/Sites/readability/node_modules/mocha/lib/runnable.js:266:21)
      at Test.Runnable.run (/Users/niko/Sites/readability/node_modules/mocha/lib/runnable.js:259:7)
      at Runner.runTest (/Users/niko/Sites/readability/node_modules/mocha/lib/runner.js:387:10)
      at /Users/niko/Sites/readability/node_modules/mocha/lib/runner.js:470:12
      at next (/Users/niko/Sites/readability/node_modules/mocha/lib/runner.js:312:14)
      at /Users/niko/Sites/readability/node_modules/mocha/lib/runner.js:322:7
      at next (/Users/niko/Sites/readability/node_modules/mocha/lib/runner.js:257:23)
      at Object._onImmediate (/Users/niko/Sites/readability/node_modules/mocha/lib/runner.js:289:5)
      at processImmediate [as _immediateCallback] (timers.js:354:15)

Cannot scrape/display certain domains due to cross-origin or framing policies

I'm running the compare-view script locally at http://localhost:3000/compare-view/ and it seems to be failing to display/scrape certain pages due to X-Frame-Options restrictions. Not entirely sure if that is because we're trying to iframe amazon.com and github.com, or if there is something deeper, but it'd be nice if we could still show the scraped contents on the right and maybe an empty frame on the left.

TIME DESC LOCATION
15:16:39.640 Load denied by X-Frame-Options: http://www.amazon.com/ does not permit cross-origin framing.
15:16:39.778 TypeError: Argument 1 of XMLSerializer.serializeToString is not an object. compare.js:7:19
15:17:53.921 Load denied by X-Frame-Options: https://github.com/mozilla/readability does not permit framing.
15:17:54.039 TypeError: Argument 1 of XMLSerializer.serializeToString is not an object. compare.js:7:19
15:24:28.204 Load denied by X-Frame-Options: https://www.mozilla.org/en-US/ does not permit framing.
15:24:28.294 TypeError: Argument 1 of XMLSerializer.serializeToString is not an object. compare.js:7:19

Some images are still erroneously stripped out from content

Test case: https://medium.com/backchannel/inside-the-deep-web-drug-lab-9718cd0fe504

Original:

<figure name="c4e6" id="c4e6" class="graf--figure postField--fillWidthImage">
    <div class="aspectRatioPlaceholder is-locked">
        <div class="aspect-ratio-fill" style="padding-bottom: 66.8%;"></div>
        <img class="graf-image" data-image-id="1*4gN1-fzOwCniw-DbqQjDeQ.jpeg"
        data-width="2100" data-height="1402" src="https://d262ilb51hltx0.cloudfront.net/max/2000/1*4gN1-fzOwCniw-DbqQjDeQ.jpeg">
    </div>
    <figcaption class="imageCaption">Cristina Gil Lladanosa, at the Barcelona testing lab | photo by Joan Bardeletti</figcaption>
</figure>

Processed:

<figure name="c4e6" id="c4e6" class="graf--figure postField--fillWidthImage">
    <figcaption class="imageCaption">Cristina Gil Lladanosa, at the Barcelona testing lab | photo by Joan Bardeletti</figcaption>
</figure>

Add method to convert a URI string to a uri object?

I've always been a bit confused by this uri object thing:

var uri = {
  spec: "http://fakehost/test/page.html",
  host: "fakehost",
  prePath: "http://fakehost",
  scheme: "http",
  pathBase: "http://fakehost/test"
};
var result = new window.Readability(uri, window.document).parse();

It seems that all the fields can be extracted from the spec string. It'd be nice if there were just some helper function which converts a fully qualified URL into a host, prePath, scheme, and pathBase for you.

Or maybe modify the window.Readability() method to accept an object or string, and if it's a URI string, try and auto-convert it to an object.

I'm guessing this would be pretty trivial w/ Node.js' url.parse() function, but it may fit better in the Readability.js library as plain olde JavaScript.

var url = require('url');

function uriFromString(str) {
  var urlObj = url.parse(str, true);
  var protocol = urlObj.protocol.replace(':', '');
  var prePath = protocol + '://' + urlObj.hostname;
  var uri = {
    spec: urlObj.href,
    host: urlObj.hostname,
    pathBase: prePath + urlObj.pathname.replace(/[^\/]+$/, ''),
    prePath: prePath,
    scheme: protocol
  };
  return uri;
}

console.log(JSON.stringify(uriFromString(uri.spec), null, 2));

I think we've also used http://medialize.github.io/URI.js/ in other projects (via Bower).

Let Readability.parse() also return the uri

It is convenient for calling code to get the uri back as part of the parse() response. Specially on iOS where it means that the calling code has to keep less state and can just work with whatever Readability.parse() returns.

Add the "simple check" script to the repo

From #77

Note that on Android we ran this parse on every page load (to see if it was worth/possible offering reader mode), and we couldn't manipulate the real DOM because that'd obviously change the page, so there was a need to serialize+reparse anyway.

We don't do that anymore on desktop because doing that caused a boatload of perf regressions. So we have a simpler check that determines whether we show the button, which isn't 1:1 equivalent to whether the content is reader-able, but it's close. From that perspective, in principle we could reuse the DOM.

If Mozilla came to the conclusion that Readability is only practical if a simple check is used beforehand (instead of running readability on every page), I believe it might be worth adding this simpler check to this repo.
This will help other reusers too (including the iOS browser as I understand it).

Recursively check to see if child nodes are a byline before removing a node

Follow-up from #40:

With the new _removeAndGetNext logic, I realize this still has the problem of not checking the children of nodes that we're about to remove (e.g. digging through a header node to look for a byline). Perhaps we should try recursively looking through nodes before removing them if we haven't found a byline yet.

Replace occurrences of let with var

The current version uses let. Unfortunately let is not available on WebKit so this issue is about replacing all occurrences of let with var.

This makes me a little sad but I think there is no other way around this.

demo page for working with CSS

As we want to make changes to the reader mode CSS it would be helpful if the UX team had an easy way to view the reader mode content and edit the CSS live so they can make adjustments using familiar browser developer tools.

I'm thinking of the WordPress Theme Unit Test as inspiration, which gives designers a view into all element variations a WordPress theme needs to support.

My initial idea was that we use a gh-pages branch in this repo to create a mozilla readability page on github with variations for Android, iOS, and Desktop. While this repo doesn't contain the CSS and doesn't want pull requests against it for CSS issues I still think this repo would probably make the best central place for the CSS work. Perhaps the gh-pages can make it very clear where and how to file bugs for each platforms CSS issues.

A simple script could pull down the various CSS styles to the gh-pages branch. Marking the last time the CSS styles were updated would be a nice thing.

Here are the current locations I'm aware of:
Desktop CSS
Android CSS
iOS CSS

I'm not sure what the best method would be to create a test page. Generating one from a complex HTML page might be a good option that could update as the JS code is updated.

Updating the page would be a manual process, but hopefully with a simple gulp or similar script it could be a quick run and push to the branch to update things.

Thoughts?

JSDOMParser doesn't handle boolean attributes

In the example below, allowfullscreen is what the html spec calls a boolean attribute:

<iframe width="560" height="315" 
  src="https://www.youtube-nocookie.com/embed/zOduYvDupXQ" 
  frameborder="0" 
  allowfullscreen></iframe>

Trying to parse this brings:

JSDOMParser error: expected '</iframe>'

Switching to

<iframe width="560" height="315" 
  src="https://www.youtube-nocookie.com/embed/zOduYvDupXQ" 
  frameborder="0" 
  allowfullscreen="allowfullscreen"></iframe>

makes it pass. We should probably support boolean attributes as they're widely used.

Wordpress and Dotclear blogging platforms, random or no reader mode

Most recent Youtube embedded markup isn't supported

Sample most recent embedded markup format:

<iframe width="560" height="315" 
  src="https://www.youtube.com/embed/zOduYvDupXQ" 
  frameborder="0" 
  allowfullscreen></iframe>

As we're stripping out all iframes, this code is removed as well, resulting in end user missing embedded youtube videos in generated pages.

Note that Youtube offers a privacy-compliant version of the markup, which uses another domain to serve the iframe content:

<iframe width="560" height="315" 
  src="https://www.youtube-nocookie.com/embed/zOduYvDupXQ" 
  frameborder="0" 
  allowfullscreen></iframe>

We should ensure that both versions are supported. I'd go as far as forcing the switch to the privacy-compliant version by default for generated readable content, though that's worth discussing.

Add ESLint or something to lint the Readability.js file

Currently I'm a big fan of ESLint, so here's my local .eslintrc file and the output from my random collection of rules:

env:
  browser: true

rules:
  comma-spacing: 0
  curly: 0
  no-underscore-dangle: 0
  quotes: [0, single]
  space-infix-ops: 0
  space-unary-ops: 0
  strict: 0

Which currently gives us 28 errors:

$ eslint .

Readability.js
    50:6   error  'dump' is not defined                                         no-undef
    55:1   error  Missing semicolon                                             semi
   118:27  error  Expected '===' and instead saw '=='                           eqeqeq
   122:17  error  Expected '===' and instead saw '=='                           eqeqeq
   235:26  error  Expected '!==' and instead saw '!='                           eqeqeq
   262:63  error  Expected '===' and instead saw '=='                           eqeqeq
   279:27  error  Expected '===' and instead saw '=='                           eqeqeq
   281:45  error  Expected '===' and instead saw '=='                           eqeqeq
   286:14  error  sibling is already defined                                    no-redeclare
   343:13  error  i is already defined                                          no-redeclare
   346:31  error  Expected '===' and instead saw '=='                           eqeqeq
   414:23  error  node is already declared in the upper scope                   no-shadow
   414:29  error  allElements is already declared in the upper scope            no-shadow
   415:15  error  i is already declared in the upper scope                      no-shadow
   418:64  error  Expected '===' and instead saw '=='                           eqeqeq
   421:4   error  Unexpected constant condition                                 no-constant-condition
   492:21  error  i is already defined                                          no-redeclare
   595:17  error  i is already defined                                          no-redeclare
   681:12  error  children is already defined                                   no-redeclare
   682:17  error  i is already defined                                          no-redeclare
   720:22  error  Expected '===' and instead saw '=='                           eqeqeq
   776:13  error  ["description"] is better written in dot notation             dot-notation
   836:31  error  Expected '===' and instead saw '=='                           eqeqeq
   853:25  error  Expected '!==' and instead saw '!='                           eqeqeq
  1174:33  error  readyState is defined but never used                          no-unused-vars
  1223:5   error  Wrapping non-IIFE function literals in parens is unnecessary  no-wrap-func
  1262:14  error  nextPageLink is already declared in the upper scope           no-shadow
  1296:22  error  Wrapping non-IIFE function literals in parens is unnecessary  no-wrap-func

✖ 28 problems (28 errors, 0 warnings)

Fix _fixRelativeUris() to handle more cases

Source:

      <p><a href="foo/bar/barz.html">link</a></p>
      <p><a href="./foo/bar/barz.html">link</a></p>
      <p><a href="/foo/bar/barz.html">link</a></p>
      <p><a href="http://test/foo/bar/barz.html">link</a></p>
      <p><a href="https://test/foo/bar/barz.html">link</a></p>

Generated content:

        <p><a href="http://fakehost/testfoo/bar/barz.html">link</a>
        </p>
        <p><a href="http://fakehost/test./foo/bar/barz.html">link</a>
        </p>
        <p><a href="http://fakehost/foo/bar/barz.html">link</a>
        </p>
        <p><a href="http://test/foo/bar/barz.html">link</a>
        </p>
        <p><a href="https://test/foo/bar/barz.html">link</a>
        </p>

_setNodeTag should handle nested tags properly

Reference: #52 (comment)

We need to fix the _setNodeTag stuff for non-JSDOMParser, which will involve reparenting stuff (ie, below, move everything from a font tag into a different span tag and then replace the font with the span). This is unfortunate because if there are nested tags, we will be adding + removing things, which will mess up the loop. For instance:

<font><font>Hello</font></font>

We'll hit the first font first with the forEach loop, which then means we'll reparent the second one, which might mean we won't iterate over the second node? Not sure... :-\

Tested in Firefox's devtools console:

>>> f = document.documentElement.appendChild(document.createElement('font'));
<font>
>>> f.appendChild(document.createElement('font'))
<font>
>>> document.getElementsByTagName("font")
HTMLCollection [ <font>, <font> ]
>>> document.getElementsByTagName("font").forEach
undefined
>>> Array.prototype.forEach.call(document.getElementsByTagName("font"), function(f) {
>>>   var x = document.createElement("span");
>>>   Array.prototype.forEach.call(f.childNodes, function(fc) {
>>>      x.appendChild(fc);
>>>   });
>>>   f.parentNode.replaceChild(x, f);
>>> });
undefined
>>> document.getElementsByTagName("font")
HTMLCollection [ <font> ]
>>> document.getElementsByTagName("font")[0].parentNode
<span>

JSDOMParser errors on http://www.newyorker.com/magazine/2014/12/01/quiet-german

JSDOMParser error: expected '</lineargradient>'
JSDOMParser error: expected '</defs>'
JSDOMParser error: expected '</svg>'
JSDOMParser error: expected '</h1>'
JSDOMParser error: expected '</div>'
JSDOMParser error: expected '</div>'
JSDOMParser error: expected '</div>'
JSDOMParser error: expected '</div>'
JSDOMParser error: expected '</div>'
JSDOMParser error: expected '</body>'
JSDOMParser error: expected '</html>'

Publish to npm

So that other projects can install it via
npm install pick-your-name

Site-specific rules

Context: #75 (comment)

The universal approach of Readability.js is admittedly convenient, though doesn't work for every website, where markup and {js|css}-driven behavior can easily get in the way trying to extract meaningful contents. That becomes a serious issue in the case of a popular website like Fastcompany…

We should start considering adding per-site rules, in order to guarantee users to access readable contents for mainstream websites. A very simplistic approach of how a configuration could look like:

// ./rules/fastcompany.com.js
ReadabilityExt.registerRule({
  remove: [
    "p > a.people-page img",
    // … moar selectors
  ]
});

Obviously, that means JSDOMParser to support querySelectorAll and friends, though I think there's work currently ongoing on this front.

Of course we should think a little harder of how the whole thing would work, which options would make sense, the possible impact on performance and so on.

We'd have to maintain per-site rules over time as websites' markup change… this could quickly become tedious, so we should restrain a very short list of supported websites. We could also rely on a community effort, though that would mean a super easy way of doing so.

Note: This is quite possibly out of scope for 38.

Thoughts?

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.