Code Monkey home page Code Monkey logo

xq-promise's Introduction

xq-promise

An implementation of the promise and fork-join patterns for async processing in XQuery 3.1 with BaseX.

What is it?

This library implements the promise pattern as seen in many other languages and frameworks. Most notably those in the javascript community, such as jQuery and Q.js.

The pattern resolves around the idea of deferred execution through what is often called a deferred object. When an action is deferred, it returns a function, known as promise that when executed, at a later time, will perform and return the results of the work it deferred.

Since the work is deferred and can be executed at an arbitrary time. There is the ability to attach further processing at a later date but prior to actual execution, via callback functions. This may sound confusing and hard to imagine, but I 'promise' the examples that follow will make it clearer. No pun intended.

Why?

The main driver behind implementing the promise pattern was to realize parallel execution of XQuery code within a single query. If this sounds enticing, keep reading!

Thanks!

I want to give a quick thanks to the BaseX team for their wonder implementation of BaseX and XQuery. It is because of their hard work, code samples, stellar architecture and code readability that this module was made possible!

Installation

  1. Copy the xq-promise-x.jar into your basex\lib directory
  2. Install the xq-promise.xqm module found at: https://github.com/james-jw/xq-promise/src/main/xquery/xq-promise.xqm

Or use xqpm to do it for you:

xqpm xq-promise

Declaration

To use the module in your scripts simple import it like so:

import module namespace promise = 'https://github.com/james-jw/xq-promise';

Version 0.8.2-BETA

This module is currently in Beta and should be used with caution. Especially in scenarios involving the writing of sensitive data.

Dependencies

This module is dependent on BaseX.

The Basics of a Promise

In it's current iteration the library includes several methods with several overloads. The methods are as follows:

  • defer
  • when
  • is-promise
  • fork-join
  • fork

defer

defer($work as function(*), 
      $arguments as item()*, 
      $callbacks as map(*,function(*))*) 
  as function(map(*,function(*)*))

The signature may look daunting but the pattern is simple. Use the defer method to defer a piece of work for later execution by passing in a function item and the future arguments. Lets see how this works with an example:

import module namespace promise = 'https://github.com/james-jw/xq-promise';
let $greet := function($name) {
   'Hello ' || $name || '!'
}
let $promise := promise:defer($greet, 'world')
return
  $promise

In the above example, we defer the execution of the $greet method until we return $promise. Upon execution of the final line we should see hello world!.

But wait!

If you examine the output. The value returned is: function (anonymous)#1. This is not at all what we want.

This is where the power of the promise pattern starts to be realized. Formost, as mentioned prior, a promise is a function. To retrieve it's value, it must be called:

$promise()

The above modifcation will result in the expected answer: Hello world!

Callbacks

In the above example we deferred a simple piece of work and then learned how to execute it at a later time by passing in the empty sequence. Now let me introduce the real power of the promise pattern with callbacks

A callback is a function which will be executed on the success or failure of some defered work. The available callback events to subscribe to are:

then

The then helper functions accepts a method to call on success as well as one to call on failure. Neither is required. The symantics of the method are as defined by the Promise A+ specification.

It acts as a pipeline function for transforming the response over successive callback executions. Unlike the next two events, but similar to fail, this method can alter the pipeline result, and generally does.

done

Called on success.

This method has no effect on the pipeline result and thus it's return value will be discared. Its main purpose is for reacting to successful deferred execution as opposed to affecting its outcome like then does.

A common use case for done is logging.

always

Operates the same as done, except it also is called on the promise's failure, not only success.

fail

Called if the action fails. The fail callback will be provided the error as a map(*) with some 'additional' information.

map {
  'code': 'error code',
  'description': 'error description',
  'value': 'error value',
  'module': 'file',
  'line': 'line number',
  'column': 'column number',
  'additional': map {
     'deferred': 'Function item which failed. Can be used to retry the request',
     'arguments': 'The arguments provided to the failed deferred.'
   }
}

A failure occurs if any deferred work or callback function throws an exception. The fail callback allows handling and potentially mitigating these errors during a fork-join process. Without a fail callback an exception will go uncaught and cause the entire query to stop. In essense, adding a fail callback to a deferred chain, is equivalent to the catch in a try/catch clause.

Also similar to the catch clause, the fail callback has the option of returning a value as opposed to propegating or throwing an error itself.

promise:defer($work) 
  => promise:fail(function ($err) {
      if($err?code = 'XQPTY0005') then 'I fixed it!'
      else fn:error(xs:QName('local:error'), 'Unfixable error!')
  })

In the above example we see that if the $err?code returned matches XQPTY0005, error will be mitigated and the result of that chain of work will be the value I fixed it!.

The simply logic is, if a fail callback chain returns a value, the failure will be handled with the value returned from the fail callback replaced in the result.

If no suitable replacement value exists, but the error should simply be ignored. The fail callback should return the empty-sequence.

promise:defer($work) 
  => promise:fail(function ($err) {
      if($err?code = 'XQPTY0005') then () (: Ignore the error and return nothing :)
      else fn:error(xs:QName('local:error'), 'Unfixable error!')
  })

In this example the error code XQPTY0005 will result in the empty sequence ().

Ultimately, if the error code is not XQPTY0005 and thus the failure cannot be mitigated. Throwing an exception within the callback using fn:error will cause the enitre fork and query to cease.

Multiple fail callbacks

If multiple fail callbacks are added, multiple levels of error handling can be achieved. If the first callback is unable to process the error, it can itself throw an exception, which the second callback will be provided. This will continue until either a callback returns a value instead of erroring, or no further fail callbacks exist. In this ladder case, the query will cease.

Here is an example of two callbacks

promise:defer($work) 
    => promise:fail(function ($err) {
      if($err?code = 'XQPTY0005') 
      then 'I fixed it!'
      else fn:error(xs:QName('local:error'), 'Unfixable error!')}) 
    => promise:fail(function ($err) {
      if($err?description = 'Unfixable error!') 
      then 'Never say never!'
      else fn:error(xs:QName('local:error'), 'Its Fatal!')
  })

Adding callbacks

There are two ways to add callbacks:

  • During a promise's creation
  • After a promise's creation

After creation

The above example attached the callback during the call with defer; however there is another, even more powerful way. A promise can have callbacks attached after its been created!

For example:

(: Same $worker, $req as above etc... :)
let $extractListItems := function ($res as map(*)) { $res?list?* } 
let $error := function ($result as item()*) {
     trace($result, 'Request failed!') => prof:void()
}
let $retrieve := p:defer($worker, ($req, $uri))
          => p:then(parse-json(?))
          => p:fail($error)
let $extract = p:then($retrieve, $extractListItems)
return
   $extract()

Note the calls to then and fail using the arrow operator. These calls add additional callbacks to the callback chain of the existing promise returning an agumented promise. Because a new expanded promise is returned, concise chaining can be accomplished!

Also note how the $extractListItems callback is appended to the $retrieve promise, resulting in a new promise $extract, which when executed, will initiate the full chain of callbacks!

Chaining Helper Functions

Four methods, matching the callback event names, exist for attaching callbacks in a chain fashion using the arrow operator. For example:

let $retrieve := p:defer($worker, ($req, $uri))
       => p:then(parse-json(?))
       => p:then($extractlistItems)
       => p:always(trace(?))
       => p:done(file:write-text(?, $path))
       => p:fail($error)
return
   $retrieve()

Imagine we want to make a request using the standard http:send-request method and then extract the body in a single streamlined callback pipeline.

Here is how this could be accomplished using the promise pattern and a then callback:

let $req := <http:request method="GET" />
let $request := http:send-request($req, ?)
let $extract-body := function ($res) { $res[2] }
let $promise := promise:defer($request, 'http://www.google.com') 
       => promise:then($extract-body)
return
  $promise()

In the above example we attached a then callback. This callback function has the ability to transform the output of it's parent promise. With this in the mind, it should be clear that the $extract-body's return value will be retuned at the call to $promise().

In this example, since the $extract-body's input will be the result of its parent promise. The result will be the response body of the http request.

when

Another critical method in the promise pattern is the when function.

when($promises as function(map(*,function(*)), 
     $callbacks as map(*,function(*))*) 
   as function(map(*,function(*)))

The purpose of when is to combine 2 or more promised actions into a single promise. This is extremly powerful. Like the defer method disscussed earlier, the when method also returns a deferred promise, which accepts callbacks just the same.

For example:

let $write-and-return-users:= function ($name, $users) as item()* {(
      file:write($name, $users),
      $users
)}
let $extractDocName := promise:defer(doc(?), $doc-uri) 
  => promise:then($extract-name)
let $extractUsers := promise:defer(json-doc(?), $uri) 
  => promise:then($extract-list-items) 
let $users:= promise:when(($extractDocName, $extractUsers))
               => promise:then($write-and-return-users)
               => promise:fail(trace(?, 'Requesting users failed: '))
})
return
    $users() ! trace(.?username, 'Retrieved: ')

In this example, we perform two deferred actions and then merge their results in the $write-and-return-users callback. Since this item is attached to the when's promise on the then callback, its result will be seen on the call to $users().

We could continue to attach callbacks as needed until we are ready. There is no limit.

The Power of Promises and Parallel Execution

Hopefully its clear now: how to defer work for later execution, what a promise is, and how to join multiple promises. It still may not be entirely clear what the benefit this pattern has in the context of XQuery; however that is about to change.

fork-join

Let me introduce two last methods, and the whole reason I wrote this library.

fork-join($promises as function(*)*) as item()*

It is simple yet powerful. It accepts a sequence of promises, or single arity functions and executes them in a fork join fashion, spawning threads as needed depending on the work load, followed by rejoining the work on the main thread.

As seen earlier, promises can be used to build up a piece of work for later execution. With this ability, coupled with fork-join. Parallelized XQuery processing becomes a reality.

Lets see how we can use this capability by comparing a simple example involving making http requests. The example will use the promise pattern but not fork-join just yet.

import module namespace promise = 'https://github.com/james-jw/xq-promise';
let $work := http:send-request(<http:request method="GET" />, ?)
let $extract-doc := function ($res) { $res[2] }
let $extract-links := function ($res) { $res//*:a[@href => matches('^http')] }
let $promises :=
  for $uri in ((1 to 5) !  ('http://www.google.com', 'http://www.yahoo.com', 'http://www.amazon.com', 'http://cnn.com', 'http://www.msnbc.com'))
  let $defer := promise:defer($work, $uri)
       => promise:then($extract-doc)
       => promise:done(trace(?, 'Results found: ')})
       => promise:then($extract-links)
  return 
    $promises ! .()

In the above example, we use promises to queue up 25 requests and then execute them in order with:

 $promises ! .()

If you run this example in BaseX GUI and watch the output window, you will see the requests come in as the query executes. This is due to the addition of the trace? 'Results Found: ' callback.

Also notice, only one request is executed at a time. Each request must wait for the full response and processing of the previous. This is a current limitation of BaseX, since by design it runs each query in its own single thread. There are several workarounds such as splitting up the work via a master query, or using a string concatenated XQuery expression to spawn another process. Although effective, all these workarounds require extra effort and multiple components. Additionally they leave the language's domain and the context of the current query..

Luckily, with the introduction of this module xq-promise. This is no longer the case! Lets change the previous example so it uses the newly introduced fork-join method to speed up the process, by splitting the requested work into multiple threads before returning the final joined value.

Luckily the previous example already used defer so the change is only one line. Replace:

$promises ! .()

which manually executes each promise on the main thread, with:

promise:fork-join($promises)

If you watch this execute in BaseX you will quickly see its executing much faster, with multiple requests being processed at once.

On my machine, the first example without fork-join took on average 55 seconds. With fork-join this time dropped to 6 seconds!

That is a clear advantage! Playing around with compute size and max forks, which I will introduce shortly, I have been able to get this even lower, to around 2 seconds!!

Hopefully its clear now what the use cases are for fork-join and how to use it!

How to interact with shared resources

With any async process comes the possibility of synchronization problems. Fortunately, XQuery due to its immutable nature is naturally suited to this type of work. Additionally from my limited look at BaseX, the code is very thread safe. Add to this, the introduction of the promise pattern and safe multi-threading appears to be real.

There are a few things to note however when using fork-join

Never attempt to write to a database within a fork

Luckily this does not restrict you from writing to databases, it just means: compute in forks, write after you have rejoined. Fortunately you can be sure everything returned from the fork-join operation is returned on the main thread and thus is safe!

For example:

(: lots of computations chaining :)
let $result := promise:fork-join($promises)
return
  for $result in $results
  return db:add($db, $path, $result)
Do not open disc resources (databases, files) from multiple forks.

Now this may seem like a major limitation, but its not. You can still interact and even open these resources in callbacks, and thus parallelized forks, however be cautious to try and open a resource only once and hopefully in a single piece of work.

let $compute := function ($doc) {
   for sliding window $w in string-to-codepoints($doc)
   start at $spos when true()
   end at $epos when $epos - $spos = 25
   return $w
}
let $promises := db:list() ! promise:defer(db:open(?), ., map {
     'then': $compute
}
return
  promise:fork-join($promises)

Its important to note that all callbacks will be executed in the fork they originated from. So in this case, opening each database and computing the windows will occur in each fork. If you attached an additional callback after $compute, it too would execute in its origin fork, and not the master thread. Amazing!

In regards to database access, or any resources for that matter. Notice how I ensure to only open one database per fork. Although this is not a strict limitation it is a recommendation.

As an alternative, queue up the large resource prior to the fork-join and use it in the callbacks:

let $largeResource := doc('...')
let $compute :=  function ($res) {
  $res?*[. = $largeResource//name]
}
let $promises := ...
return
  promise:fork-join($promises)
Other words of caution!
  • Not everything should be parallelized.

For example, disc writes and other operations should be handled with care when using fork-join

Advanced Forking

Certain scenarios can be optimized by changing the:

  • compute size - Number of deferred jobs to process per thread
  • max forks - Max number of forked threads to allow at once.
Compute size

Setting compute size is done during the call to fork-join by providing an additional xs:integer argument

For example:

promise:fork-join($promises, 1)

The above query sets the compute size to 1.

  • The default compute size is 2.

Depending on the level of effort in performing an individual task, this option can be highly beneficial. For example, when computing millions of small computations, it may be worthwhile to increase the value significanly. For example to 1000.

On the contrary, when doing very computationally expensive tasks it may be best to leave this option alone, or even lower it to 1.

Max forks

The following query sets the compute size to 1 and the max forks to 20:

promise:fork-join($promises, 1, 20)

For some operations, such as http requests, this can decrease script execution time.

  • By default max forks is equal to the number of processor cores.

Here is the complete signature:

promse:fork-join($promises as function(*,map(*)), 
                 $compute-size as xs:integer?, 
                 $max-forks as xs:integer?) 
             as item()*
Fork in Fork?

Why not?! You may wonder if you can fork in a forked callback. The answer is YES! Generally this would not be advised however in certain scenarios this is beneficial. Since all fork-joins share the same pool, inner forks merely ensure every thread is used to it's maximum. However with anything, too many can be detrimental and is dependent on the type of work being performed.

Here is an example:

let $request := http:send-request($req, ?)
let $request-all-links := function ($res) {
  let $promises := $res//a/@href ! promise:defer($request, .)
  return
    promise:fork-join($promises)
}
let $work := 
  for $uri in $uris
  return promise:defer($request, $uri, map { 'then': $request-all-links })
return
  promise:fork-join($work)

In this case, since the inner fork-join simply makes lots of external requests, this may actually improve execution time.

Limitations

With any async process their are limitations. So far these are the only noticed limitations:

  • Updating database nodes in a callback

Unit Tests

Clone the repo and run basex -t within the repo's directory to run the unit tests.

Shout Out!

If you like what you see here please star the repo and follow me on github or linkedIn

Happy forking!!

xq-promise's People

Contributors

james-jw avatar lukask avatar mgaerber avatar

Stargazers

Janusz Dziurzyński avatar  avatar Adam Retter avatar Andreas M. avatar Tiberiu Ichim avatar Kristian Kankainen avatar Joe Wicentowski avatar Andrew Sales avatar Chris Hart avatar  avatar Carl Leitner avatar Joseph W. Crowther avatar Kourosh avatar Dino Fancellu avatar Charles Foster avatar Matti Lassila avatar Andy Bunce avatar Johan Mörén avatar dcore avatar Tim Thompson avatar Kevin S. Clarke avatar Bridger Dyson-Smith avatar Clifford Anderson avatar  avatar

Watchers

Andy Bunce avatar Joseph W. Crowther avatar Carl Leitner avatar James Fuller avatar Johan Mörén avatar James Cloos avatar  avatar Kristian Kankainen avatar  avatar

Forkers

xquery mgaerber

xq-promise's Issues

XPTY0004 issue with xq-promise 0.8.0 beta

Hi James,
I am trying this with BaseX 8.4 beta 165f5fd

import module namespace promise = 'org.jw.basex.async.xq-promise';
let $work := http:send-request(<http:request method="GET" />, ?)
let $extract-doc := function ($res) { $res[2] }
let $extract-links := function ($res) { $res//a[@href => matches('^http')] }
let $promises :=
  for $uri in ((1 to 5) !  ('http://www.google.com', 'http://www.yahoo.com', 'http://www.amazon.com', 'http://cnn.com', 'http://www.msnbc.com'))
  let $defer := promise:defer($work, $uri, map {
       'then': ($extract-doc),
       'done': trace(?, 'Results found: ')})
  return 
     promise:attach($defer, map {'then': $extract-links })
return 
promise:fork-join($promises,1)

I get the error:

[XPTY0004] XqPromise:forkJoin(item()+, xs:integer): org.basex.query.QueryContext.<init>(Lorg/basex/query/QueryContext;Z)V.

Should this work?

Calling promise fails if worker takes more than one argument

Am I doing something wrong? Or should this indeed work?

let $worker := function($fname, $lname) { 'Hello, ' || $fname || ' ' || $lname }
let $promise := promise:defer($worker, ('James', 'Wright'), map {
  'then': function($p) { 'then: ' || $p },
  'fail': function ($result as item()*) {
            trace($result, 'Request failed!') => prof:void()
          }
})
return $promise(())

Result:

Evaluating:
Request failed!James
Request failed!Wright

Expected:

then: Hello, James Wright

Same problem with a query found in README:

let $req := <http:request method="GET"/>
let $uri := 'http://www.google.com'
let $worker := http:send-request(?, ?)
let $extractListItems := function ($res as map(*)) { $res?list?* }
let $error := function ($result as item()*) {
     trace($result, 'Request failed!') => prof:void()
}
let $retrieve := promise:defer($worker, ($req, $uri), map { 
           'then': parse-json(?), 
           'fail': $error 
}) 
let $extract := $retrieve(map { 'then': $extractListItems  })
return
   $extract(())

xq-promise Terminology vs. JavaScript/jQuery

Hi @james-jw,

Very cool stuff. I finally found some time to look at this properly in detail and it is a very interesting piece of work.

Having read your paper from XML Prague and tried to understand Promise in the ECMA spec, and Deferred in the jQuery documentation, I am a little confused. I am wondering if the terminology you use in xq-promise is meant to be directly comparable to the ECMA or jQuery? For example do you consider a "Promise" in xq-promise to have the same semantics as a "Promise" in ECMA?

From what I can understand the major difference is the execution model. It is clear to me that in xq-promise a Promise is never executed until it is passed to fork-join or fork. In both EMCA and jQuery it seems that the function passed to either Promise or Deferred is immediately evaluated with the arguments necessary it to complete the promise, although that completion may happen later if the function itself calls other asyncronous functions (e.g. setTimeout).

Also it seems that in xq-promise a promise is completed by the return value of the function passed to promise:deferred being evaluated. Whereas in ECMA and jQuery, the inner function has to call the resolve or failed functions that it is passed as arguments. This is perhaps a subtle difference, but perhaps an important one?

I guess I am confused by how you use the terms executed and execution. If I understand correctly with ECMA and jQuery a Promise may have been "completed" before you compose with it via then to it, but that is okay, the result is still the same.

Just wondering what your thoughts are on this?

When should pass non function items

Currently when only accepts functions or deferred objects but regular non function values should be supported as well and be passed along unchanged to the when's callbacks.

For example:

let $doc := doc('')
let $fork := p:fork($do-something)
let $promise := p:when($doc, $fork) => p:then($do-something-else)
return 
   $promise()

promise:fork

[edited from 1st version]
Hi James,
This is not working the way I am looking for. Am I doing it wrong?

import module namespace promise = 'org.jw.basex.async.xq-promise';

let $query := 'prof:sleep(10000),admin:write-log("test1")'
let $promise := promise:fork(xquery:eval(?), $query)
return ( 2,$promise())

This does not return until the $query has completed. I want to kick off the query and forget about it.

Support for updates

I didn’t check this out so far by myself… But maybe it will get easier with your new XQuery-based solution to add support for real updates?

fail should be provided a map with error information as well as arguments bound.

When a fail callback is called. It should be provided a single argument which is map that contains all the error information.

I propose:

map {
  'err': {
     'description': Same as standard error bindings
     ...
   },
  'args': array [ ... ]
}

This way, the fail callback has the information needed to either report a useful error, including the bad arguments or continue the work somehow.

Rewriting FOR Clause

Hi James, I checked out your text in the XML Prague Conference Proceedings (Page 139 ff.), and I stumbled upon your interesting thoughts on how the FLWOR expressions could be extended to allow for an asynchronous execution. I wrote down some more examples, and I tried to formalize it:

Query 1:

for async $a in 1 to 2
return $a

Query 1 (rewritten):

(: return :) p:fork-join(
  for $a in 1 to 2
  return function() { (: return :) $a }
)

Query 2:

for       $a in 1 to 2
for async $b in 3 to 4
return $a * $b

Query 2 (rewritten):

for $a in 1 to 2
return p:fork-join(
  for $b in 3 to 4
  return function() { (: return :) $a * $b }
)

Query 3:

for async $a in 1 to 2
for       $b in 3 to 4
return $a * $b

Query 3 (rewritten):

(: return :) p:fork-join(
  for $a in 1 to 2
  return function() {
    for $b in 3 to 4
    return $a * $b
  }
)

Query 4:

for async $a in 1 to 2
for async $b in 3 to 4
return $a * $b

Query 4 (rewritten):

(: return :) p:fork-join(
  for $a in 1 to 2
  return function() {
    p:fork-join(
      for $b in 3 to 4
      return function() { (: return :) $a * $b }
    )
  }
)

Query 5:

for       $a in 1 to 2
for async $b in 3 to 4
order by $b descending
return ($a * $b)

Query 5 (rewritten):

…does not work out, as the rewritten version would have a nested FLWOR expression, which will lead to different results. Similar problems arise with count, group by and window clause. A pragmatic solution would be to limit the usage of async to cases in which all following clauses are for, let and return.

As a result, we might end up with the following rewriting rules:

  • All for clauses in a FLWOR expression are checked for the async keyword; the most inner clause will be checked first.
  • If a for clause has the async keyword, it will be replaced with a return clause, and the expression of this clause will be a p:fork-join function call.
  • The argument of p:fork-join will be a new FLWOR expression, which consists of:
    • the original, replaced for clause (excluding the async keyword), and
    • a return clause with a function item as expression, which contains the remaining clauses of the original FLWOR expression as function body.
  • If any of the FLWOR expressions (the original one, or the newly created ones) only consists of a return clause, it must be simplified and replaced with its expression (e.g.: return p:fork-join(...)p:fork-join(...); return function() { $a }function() { $a }).

As I’ve surely missed something, anyone’s input is welcome!

Promise Module → Async Module?

You convinced me in indicating that the fork-join code is indeed not dependent on the promise pattern, so I’m currently wondering if we should define an Async Module (alternative names are welcome), which includes your promise:fork-join function, and possibly some more functions based on the ideas that have been developed by @apb2006 and discussed in BaseXdb/basex#1211. Do you think that would make sense? We could e.g. define this module as W3C EXPath Module:

(: Namespace: async = 'http://expath.org/spec/async' :)
async:fork-join($functions as function(*))) as item()*,
async:fork-join($functions as function(*)), $options as map(*)) as item()*

Keys allowed for $options:

  • threads (?): number of threads to allow in the pool (default, maybe implementation-defined: number of CPU cores available on the current system)
  • thread-size (?): number of functions to be evaluated by each thread (default: 1)

pure xquery implementation

Looking at the java component of this impl I was wondering what the barriers were to a pure xquery solution ?

ps- enjoyed your talk at XML Prague

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.