Giter Site home page Giter Site logo

erlcron's Introduction

Erlcron

Build Status Hex pm Docs

Erlcron provides testable cron like functionality for Erlang systems, with the ability to arbitrarily set the time and place along with fastforwarding through tests. See erlcron.erl for more documentation.

The syntax of a job description is quite different from crontab. It is (in this author's opinion) easier to read and is much more in keeping with the Erlang tradition. It is not quite as expressive as cron but this can be compensated for by adding multiple jobs.

No output is logged or mailed to anyone. If you want output to be logged or mailed, you must explicitly specify that as part of the job.

This does not poll the system on a minute-by-minute basis like cron does. Each job is assigned to a single (Erlang) process. The time until it is to run next is calculated, and the process sleeps for exactly that long.

Unlike cron's one-minute resolution, erlcron has a millisecond resolution.

It does handle Daylight Savings Time changes (but not the cases when the system clock is altered by small increments, in which case the next execution of a scheduled task may be imprecise).

Cron Job Description Examples:

{{once, {3, 30, pm}, fun() -> io:fwrite("Hello world!~n") end}

{{once, {3, 30, pm}, fun(JobRef, CurrDateTime) -> io:fwrite("Hello world!~n") end}

{{once, {3, 30, pm}},
    {io, fwrite, ["Hello, world!~n"]}}

{{once, {12, 23, 32}},
    {io, fwrite, ["Hello, world!~n"]}}

{{once, 3600},
    {io, fwrite, ["Hello, world!~n"]}}

{{daily, {every, {23, sec}, {between, {3, pm}, {3, 30, pm}}}},
    {io, fwrite, ["Hello, world!~n"]}}

{{daily, {3, 30, pm}},
    fun(_JobRef, _DateTime) -> io:fwrite("It's three thirty~n") end}

{{daily, [{1, 10, am}, {1, 07, 30, am}]},
    {io, fwrite, ["Bing~n"]}}

{{weekly, thu, {2, am}},
    {io, fwrite, ["It's 2 Thursday morning~n"]}}

{{weekly, wed, {2, am}},
    {fun(_JobRef, _DateTime) -> io:fwrite("It's 2 Wednesday morning~n") end}

{{weekly, [tue,wed], {2, am}},
    {fun(_, Now) -> io:format("Now is ~p~n", [Now]) end}

{{weekly, fri, {2, am}},
    {io, fwrite, ["It's 2 Friday morning~n"]}}

{{monthly, 1, {2, am}},
    {io, fwrite, ["First of the month!~n"]}}

{{monthly, [1, 7, 14], {2, am}},
    {io, fwrite, ["Every 1st, 7th, 14th of the month!~n"]}}

{{monthly, 0, {2, am}},
    {io, fwrite, ["Last day of the month!~n"]}}

{{monthly, [0, -1], {2, am}},
    {io, fwrite, ["Last two days of the month!~n"]}}

{{monthly, 4, {2, am}},
    {io, fwrite, ["Fourth of the month!~n"]}}

%% Days of month less or equal to zero are subtracted from the end of the month
{{monthly, 0, {2, am}},
    {io, fwrite, ["Last day of the month!~n"]}}

Adding a cron to the system:

Job = {{weekly, thu, {2, am}},
    {io, fwrite, ["It's 2 Thursday morning~n"]}}.

erlcron:cron(Job).

Cron jobs can be given named atom references:

erlcron:cron(test_job1, {{once, {3,pm}}, {io, fwrite, "It's 3pm"}}).

A simple way to add a daily cron:

erlcron:daily({3, 30, pm}, Fun).

A simple way to add a job that will be run once in the future. Where 'once' is the number of seconds until it runs.

erlcron:at(300, Fun).

Cancel a running job.

erlcron:cancel(JobRef).

Get the current reference (universal) date time of the system.

erlcron:datetime().

Set the current date time of the system. Any tests that need to be run in the interim will be run as the time rolls forward.

erlcron:set_datetime(DateTime).

Set the current date time of the system on all nodes in the cluster. Any tests that need to be run in the interim will be run as the time rolls forward.

erlcron:multi_set_datetime(DateTime).

Set the current date time of the system on a specific set of nodes in the cluster. Any tests that need to be run in the interim will be run as the time rolls forward.

erlcron:multi_set_datetime(Nodes, DateTime).

The application cron can be pre-configured through environment variables in the config file that all applications can load in the Erlang VM start. The app.config file can be as follow:

[
    {erlcron, [
        {crontab, [
            {{once, {3, 30, pm}}, {io, fwrite, ["Hello, world!~n"]}},

            %% A job may be specified to be run only if the current host
            %% is in the list of given hostnames.  If default crontab
            %% options are provided in the `defaults` setting,
            %% the job-specific options will override corresponding
            %% default options.  See erlcron:job_opts() for available
            %% options.
            {{once, {12, 23, 32}}, {io, fwrite, ["Hello, world!~n"]},
                #{hostnames => ["somehost"]},

            {{daily, {every, {23, sec}, {between, {3, pm}, {3, 30, pm}}}},
             {io, fwrite, ["Hello, world!~n"]}},

            %% A job spec can be defined as a map, where the `interval' and
            %% `execute' keys are mandatory:
            #{id => test_job,    interval  => {daily, {1, 0, pm}},
                                 execute   => {io, fwrite, ["Hello, world!~n"]}},

            %% If defined as a map, the map can contain any `erlcron:job_opts()'
            %% options:
            #{id => another_job, interval  => {daily, {1, 0, pm}},
                                 execute   => {io, fwrite, ["Hello, world!~n"]},
                                 hostnames => ["myhost"]}
        ]},

        %% Instead of specifying individual options for each job, you can
        %% define default options here.
        {defaults, #{
            %% Limit jobs to run only on the given list of hosts
            hostnames    => ["myhost"],

            %% Function `fun((Ref) -> ignore | any())` to call before a job is started.
            %% If the function returns `ignore`, the job will not be executed, and the
            %% `on_job_end` callback will not be executed.
            on_job_start => {some_module, function},

            %% Function `fun((Ref, Status :: {ok, Result}|{error, {Reason, StackTrace}}) -> ok)`
            %% to call after a job has ended
            on_job_end   => {some_module, function}
        }}
    ]}
].

So, when the app will be started, all configurations will be loaded.

Note that the limitation is that in the config file is impossible load an anonymous function (or lambda function) so, you only can use {M,F,A} format.

If an on_job_start or on_job_end functions are provided for any job or as default settings, it's the responsibility of a developer to make sure that those functions handle exceptions, since the failure in those functions will cause the supervisor to restart the job up until the supervisor reaches its maximum restart intensity.

erlcron's People

Contributors

akatro avatar cjkjellander avatar drobakowski avatar ericbmerritt avatar jgordor avatar jwilberding avatar kianmeng avatar manuel-rubio avatar mattiasw2 avatar maximvl avatar randomseeded avatar saleyn avatar santiniis avatar sasa1977 avatar srijan avatar tomjnixon avatar tsloughter avatar vkatsuba 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

erlcron's Issues

Fix -spec's for OTP-19

The old -spec(function/arity :: ...) format is not supported in OTP-19, so I have replaces those occurrences with the -spec function(...) notation.

The documentation and last release are out of sync

If you try to use a tagged release (for example, in rebar), the documented behavior of putting cron job definitions into app.config doesn't work since setup wasn't implemented. It would be nice if you could tag the current master as 0.4 or something like that so there's something stable to use in rebar.

Succesive calls to erlcron:datetime() return the same date

Calls to erlcron:datetime() always return the same date/time, even though they are performed seconds apart.

2> erlcron:datetime().
{{{2015,9,23},{12,30,3}},1443004203}
3> erlcron:datetime().
{{{2015,9,23},{12,30,3}},1443004203}
4> erlcron:datetime().
{{{2015,9,23},{12,30,3}},1443004203}

However, job execution seems to be working fine

Jobs scheduled using 'daily' should work from today not from the next day

Following snippet suppose to print "Hello world!" every 5 sec. However it would do so only after midnight.

Job = {{daily, {every, {5, sec}, {between, {5, pm}, {11, 59, pm}}}}, {io, format, ["Hello, world!~n"]}}.
erlcron:set_datetime({{2013, 11, 5}, {20, 30, 17}}).
Ref = erlcron:cron(Job).

Few debugging statements added to here [https://github.com/erlware/erlcron/blob/master/src/ecrn_agent.erl#L248:L256] show following:

period: [28800,28810,...,66580,66590]
start: 28800
end: 66590
current_time: 73817

syntax error before: '/'

This might be something that i'm doing wrong but when i try to build erlcron using erlang.mk (adding it to my Makefiles DEPS line) i get the following error:

Happy to do more testing or send more info if it helps

make
DEP erlcron
DEP rebar_vsn_plugin
DEPEND rebar_vsn_plugin.d
ERLC rebar_vsn_plugin.erl
APP rebar_vsn_plugin.app.src
sed "s/{{EXTRA_OPTS}}/ {d,namespaced_types},/" Emakefile.src > Emakefile
erl -noinput -eval 'up_to_date = make:all()' -s erlang halt
make[2]: Nothing to be done for compile'. make[2]: Nothing to be done forcompile'.
make[2]: Nothing to be done for compile'. make[2]: Nothing to be done forcompile'.
DEPEND erlcron.d
ERLC ecrn_agent.erl ecrn_app.erl ecrn_control.erl ecrn_cron_sup.erl ecrn_reg.erl ecrn_sup.erl ecrn_test.erl ecrn_util.erl erlcron.erl
src/ecrn_agent.erl:48: syntax error before: '/'
src/ecrn_agent.erl:53: syntax error before: '/'
src/ecrn_agent.erl:57: syntax error before: '/'
src/ecrn_agent.erl:61: syntax error before: '/'
src/ecrn_agent.erl:65: syntax error before: '/'
src/ecrn_agent.erl:71: syntax error before: '/'
src/ecrn_agent.erl:167: syntax error before: '/'
src/ecrn_agent.erl:181: syntax error before: '/'
src/ecrn_agent.erl:203: syntax error before: '/'
src/ecrn_agent.erl:247: syntax error before: '/'
src/ecrn_agent.erl:259: syntax error before: '/'
src/ecrn_agent.erl:265: syntax error before: '/'
src/ecrn_agent.erl:270: syntax error before: '/'
src/ecrn_agent.erl:283: syntax error before: '/'
src/ecrn_agent.erl:302: syntax error before: '/'
src/ecrn_agent.erl:319: syntax error before: '/'
src/ecrn_agent.erl:329: syntax error before: '/'
src/ecrn_agent.erl:347: syntax error before: '/'
src/ecrn_agent.erl:353: syntax error before: '/'
src/ecrn_agent.erl:360: syntax error before: '/'
src/ecrn_agent.erl:40: Warning: type state() is unused
make[2]: *** [ebin/erlcron.app] Error 1
make[1]: *** [app] Error 2
make: *** [deps] Error 2

find a bug

do_job_run(State, {, Job})
when is_record(State, state), is_function(Job) ->
RunFun = fun() ->
Job(State#state.alarm_ref, current_date(State))
end,
proc_lib:spawn(RunFun);
do_job_run(State, {
, {M, F, A}})
when is_record(State, state) ->
proc_lib:spawn(M, F, A).

If Job is "fun() -> io:format("hello world!") end", the statment "Job(State#state.alarm_ref, current_date(State))" will has badarity error.

Inaccurate calculation of monthly timeout/apply time.

The calculation of the timeout is inaccurate when it comes to monthly and the time has already passed for today, For example today is the second I want to run a process on the 1st of next month, at 1 am, however it is already 1pm today.

If I do ecrn_agent:until_next_milliseconds(NewState, Job).
{ok,2634180000}

That gives me ~= 30 day 11 hr. 43 min.

ecrn_agent:until_next_daytime_or_days_from_now(NewState , {1,5,0}, 30).
2547909

which when I convert that to milliseconds is ~= 29 day 11 hr. 45 min. 9 sec. Which seems to be correct according to my manual calculation.

So I propose in ecrn_agent changing the following code.

case Today of
DoM ->
until_next_daytime_or_days_from_now(State, Period, Days);
_ ->
until_days_from_now(State, Period, Days)
end

and replace with

until_next_daytime_or_days_from_now(State, Period, Days)

Making the complete function the following.

until_next_time(State, {{monthly, DoM, Period}, _What}) ->
{{ThisYear, ThisMonth, Today}, _} = current_date(State),

Days = case calendar:date_to_gregorian_days(ThisYear, ThisMonth, DoM)  - calendar:date_to_gregorian_days(ThisYear, ThisMonth, Today) of
    Res when Res < 0 ->
            {NextYear, NextMonth} =
            case ThisMonth of
                12 ->
                    {ThisYear + 1, 1};
                _  ->
                    {ThisYear, ThisMonth + 1}
            end,
            D1 = calendar:date_to_gregorian_days(ThisYear, ThisMonth, Today),
            D2 = calendar:date_to_gregorian_days(NextYear, NextMonth, DoM),
            D2 - D1;
   Ds ->
       Ds
end,

until_next_daytime_or_days_from_now(State, Period, Days).

erlcron conflicts with the UNIX at command

In UNIX, the at command allows users to run a job once at a particular time. In erlcron, it schedules something to occur daily.

This behaviour is confusing. I recommending changing the erlcron function to daily and adding a shortcut for a single-shot job that is being called in the future that is called at.

Scheduling a job in the past crashes erlcron

Hi,

I've found that if you try and schedule a task to run with erlcron:once in the past, the application crashes. The supervisor is expecting to receive {ok,} but it receives {error,} which then crashes erlcron and brings down the whole application. Here's the code (ecrn_cron_sup.erl):

%% @doc
%%  Add a chron job to be supervised
-spec add_job/2 :: (erlcron:job_ref(), erlcron:job()) -> erlcron:job_ref().
add_job(JobRef, Task) ->
{ok, _} = supervisor:start_child(?SERVER, [JobRef, Task]),
JobRef.

This is an issue because say on start up I schedule a task to be run at midday. This runs and all goes well, but I restart the app for some reason at 12:30. When the application starts back up, it will crash because erlcron crashes due to the time being in the past.

The canonical behaviour in Unix cron is to simply not run a task that is in the past. I believe that erlcron should probably mimic this behaviour too. If I schedule a task for 2 minutes in the past, it should simply not run.

At the least there should be some way of communicating that the task won't actually run rather than the application crashing.

That said I'm not sure what the desired behaviour really should be and so I'm open to any suggestions about how else I can handle the problem...

Thanks in advance!

Problems if the cron process crash or is canced.

% Start the application.
1> application:start(erlcron).
ok

% Start a job and get its Pid through job_ref.
2> f(),Ref=erlcron:cron({{once,100},{io,format,["hungry"]}}),{ok,[Pid]}=ecrn_reg:get(Ref).
{ok,[<0.60.0>]}

% Kill the Pid.
3> exit(pid(0,60,0),kill).
true

% Get the Pid.
4> ecrn_reg:get(Ref).
{ok,[<0.60.0>,<0.62.0>]}

% Kill the Pid.
5> exit(pid(0,62,0),kill).
true

% Get the Pid.
6> ecrn_reg:get(Ref).
{ok,[<0.60.0>,<0.62.0>,<0.65.0>]}

% After 100 seconds, the shell will print hungry.
7> hungry

So here comes the problem, I wish to run a task after 100 seconds, but unfortunately, the cron process crashes after 50 seconds, and then restart. Finally, the task will be run after 150 seconds.

1> application:start(erlcron).
ok

2> f(),Ref=erlcron:cron({{once,100},{io,format,["hungry"]}}),{ok,[Pid]}=ecrn_reg:get(Ref).
{ok,[<0.60.0>]}

3> exit(pid(0,60,0),kill).
true

4> ecrn_reg:get(Ref).
{ok,[<0.60.0>,<0.62.0>]}

5> exit(pid(0,62,0),kill).
true

6> ecrn_reg:get(Ref).
{ok,[<0.60.0>,<0.62.0>,<0.65.0>]}

7> erlcron:cancel(Ref).

=ERROR REPORT==== 14-Jan-2018::18:20:18 ===
** Generic server ecrn_control terminating
** Last message in was {cancel,#Ref<0.0.1.111>}
** When Server state == {state,{{2018,1,14},{18,18,46}},1515982726}
** Reason for termination ==
** {{case_clause,{ok,[<0.60.0>,<0.62.0>,<0.65.0>]}},
[{ecrn_control,internal_cancel,1,
[{file,"src/ecrn_control.erl"},{line,111}]},
{ecrn_control,handle_call,3,[{file,"src/ecrn_control.erl"},{line,68}]},
{gen_server,try_handle_call,4,[{file,"gen_server.erl"},{line,629}]},
{gen_server,handle_msg,5,[{file,"gen_server.erl"},{line,661}]},
{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,240}]}]}
** exception exit: {{{case_clause,{ok,[<0.60.0>,<0.62.0>,<0.65.0>]}},
[{ecrn_control,internal_cancel,1,
[{file,"src/ecrn_control.erl"},{line,111}]},
{ecrn_control,handle_call,3,
[{file,"src/ecrn_control.erl"},{line,68}]},
{gen_server,try_handle_call,4,
[{file,"gen_server.erl"},{line,629}]},
{gen_server,handle_msg,5,
[{file,"gen_server.erl"},{line,661}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,240}]}]},
{gen_server,call,[ecrn_control,{cancel,#Ref<0.0.1.111>}]}}
in function gen_server:call/2 (gen_server.erl, line 204)

In this case, if I cancel this job, it will cause an exception, which I think should be fixed.

Is this the canonical erlcron?

There are several forks of erlcron which make rather small changes, such as adding rebar or removing eunit dependency. Being rather new to this world (Erlang development), I can't really evaluate the validity of these forks beyond the fact that rebar integration is essential for me at the moment.

My question is whether this, the erlware original erlcron (which does seem to be derived fron crone, no?), should be considered the mother ship, and that contributions should be oriented towards eventual merging here? There has been no activity here for a couple of years, but on the other hand I don't see any pull requests from these more recent forks.

In fact, being new to github, at least as a participant, I'm not very knowledgable yet about the protocol for managing my relationship with projects like this. E.g. if erlware won't be adding rebar compatibility, should I hunt down a fork that has done this and try to form a relationship with that entity? Or best to just create a fork myself and then just see what happens down the road? (Since I can always merge in changes from the mother ship if necessary.)

Thanks,
Erik.

test fails

It seems one of the tests is failing.. The relevant part of the output is pasted below:

  ecrn_test: cron_test_...*failed*
in function ecrn_test:'-validation_test/1-fun-0-'/0 (src/ecrn_test.erl, line 150)
in call from ecrn_test:validation_test/1 (src/ecrn_test.erl, line 150)
**error:{assertMatch,[{module,ecrn_test},
              {line,150},
              {expression,"ecrn_agent : validate ( { once , { 3 , 30 , pm } } )"},
              {pattern,"valid"},
              {value,invalid}]}
  output:<<"">>


=INFO REPORT==== 29-May-2018::08:42:51 ===
    application: erlcron
    exited: stopped
    type: temporary
  [done in 4.241 s]
module 'ecrn_sup'
module 'ecrn_reg'
  ecrn_reg: generate_test_...[0.001 s] ok
  [done in 0.005 s]
module 'ecrn_cron_sup'
module 'ecrn_control'
module 'ecrn_app'
module 'ecrn_agent'
=======================================================
  Failed: 1.  Skipped: 0.  Passed: 6.


{tag, "master"} ?

That's in rebar.config

{deps, [{rebar_vsn_plugin, ".*",
{git, "https://github.com/erlware/rebar_vsn_plugin.git",
{tag, "master"}}}]}.

Can you fix it?

Timeout with jobs monthly R16B02

When ecrn_agent:init/1 is setting up a monthly job it crashes because the timeout is too long.

The timeout i calculated for example is
4835019000

You can simulate this error by using the following

receive void -> void after 4835019000 -> time end.

** exception error: bad receive timeout value
in function prim_eval:'receive'/2

Add milliseconds granularity to cron durations

With the core rewritten, ensuring milliseconds granularity, there is a good possibility that erlcron can support durations lower than a second.

As such, taking care of this issue should simply be adding a new function clause to ecrn_agent:resolve_dur, in which it receives an atom identifying that it is a millisecond granularity, and the amount of milliseconds,, and returning it as is

Push lib to hex.pm

Hi All,

Please push this lib to the hex.pm. Thanks!

Best Regards,
--V

Looking for maintainer

Sorry I didn't do this earlier, is anyone interesting in taking this project?

I do not use it and have never looked at it. I noticed it had recent issues/pull requests so took a look and seems there are people using it and possibly interested in taking ownership.

Add functionality for handling "unsafe" jobs

Feature proposal

Currently, erlcron supports the handling of arbitrary function calls to Erlang. It would be nice to provide a convenience layer for handling external scripts (via os:cmd/1 for example).

Things to take care of:

  • Actual script execution and API
  • Storing the PID for cancellation purposes
  • Providing a cancel function that terminates the stored PID and/or all child PIDs spawned
  • Piping process output to a log somewhere so that erlcron can return the log after being given a jobref

I am uncertain of the following:

  • OS compatibility (I use linux primarily with a bit of osx)
  • Job ref syntax

This feature will have to make some assumptions about the external service being run. Thoughts?

Application fails to start when jobs are defined in the config file

When erlcron:cron/2 successfully starts a job, it returns a job_ref():

erlcron/src/erlcron.erl

Lines 131 to 132 in 00e9068

-spec cron(job()|job_ref(), job()|cron_opts()) ->
job_ref() | ignored | already_started | {error, term()}.

When starting jobs during application startup, it expects erlcron:cron/2 to return ok for success:

erlcron/src/ecrn_app.erl

Lines 60 to 69 in 00e9068

case erlcron:cron(CronJob, Def) of
ok ->
ok;
already_started ->
ok;
ignored ->
ok;
{error, Reason} ->
erlang:error({failed_to_add_cron_job, CronJob, Reason})
end

This missmatch in expectations causes the erlcron application startup to fail in the presense of jobs defined in the config file.

License is a bit weird

When I open-sourced crone I used a BSD license boilerplate that was supplied by the OSI, which had a technical problem: it still mentioned "THE REGENTS" in the disclaimer text, even though they had replaced "Regents" with "Copyright holders" in the legal condition text itself. ("Regents" originally referred to the Regents of the University of California at Berkeley.)

I've since replaced that BSD license on crone with one that does not include the word "REGENTS". Feel free to use that instead.

Also, your own license terms appear to be a verbatim copy of crone's original license terms. Of course, this means it has the "REGENTS" problem. But it also contains "Neither the name of Cat's Eye Technologies", which I don't think you want when the copyright belongs to eCD Market.

crone's new license is less error-prone this way as it just says "the names of the copyright holders" for that bit. I might suggest just using one copy of the license text, with the names of all the copyright holders at the top.

erlcron needs more accessible documentation

Right now Erlcron is pretty well documented in the modules. However that documentation is not super accessible. With that in mind, we need to figure out where the documentation should go (wiki?) and get it there.

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.