Giter Club home page Giter Club logo

alejandra's Introduction

Alejandra πŸ’…

The Uncompromising Nix Code Formatter

CI/CD Coverage License: The Unlicense style: Alejandra

Try it on your browser! here

Features

  • βœ”οΈ Fast

    It's written in Rust and formats Nixpkgs in just a few seconds. 1

  • βœ”οΈ Powerful

    We define a comprehensive style for all possible combinations of the Nix expression language.

  • βœ”οΈ Reliable

    High coverage, battle tested.

    From Nix's eyes, code is just the same. 2

  • βœ”οΈ Beautiful

    Beauty is subjective, right?

    We started from the original style of Nixpkgs, and then we applied the feedback of developers who have used Nix at scale for several years, producing a very well-grounded style guide.

  • βœ”οΈ Transparent

    You won't notice the formatter after a while.

    Humans care about the content, machines about the style!

  • βœ”οΈ Native

    We integrate with common code editors and workflows:

Getting started

On the web editor

Please visit: kamadorueda.github.io/alejandra.

Prebuilt binaries

You can download a binary for your platform:

Make it executable ($ chmod +x) and run Alejandra with:

$ ./alejandra --help

or:

$ /path/to/alejandra --help

From Nixpkgs

Please visit: search.nixos.org/packages?query=alejandra.

Nix installation

  • Nix stable:

    $ nix-env -ivf https://github.com/kamadorueda/alejandra/tarball/3.0.0
  • Nix with Flakes:

    $ nix profile install github:kamadorueda/alejandra/3.0.0

Then run Alejandra with:

$ alejandra --help

NixOS installation

  • Nix stable:

    let
      alejandra =
        (import (builtins.fetchTarball {
          url = "https://github.com/kamadorueda/alejandra/tarball/3.0.0";
          sha256 = "0000000000000000000000000000000000000000000000000000";
        }) {})
        .outPath;
    in {
      environment.systemPackages = [alejandra];
    }
  • Nix with Flakes:

    {
      inputs = {
        nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    
        alejandra.url = "github:kamadorueda/alejandra/3.0.0";
        alejandra.inputs.nixpkgs.follows = "nixpkgs";
      };
    
      outputs = {alejandra, nixpkgs, ...}: {
        nixosConfigurations = {
          example = nixpkgs.lib.nixosSystem rec {
            # We support: aarch64-darwin, aarch64-linux, i686-linux, x86_64-darwin, x86_64-linux
            system = "x86_64-linux";
    
            modules = [
              {
                environment.systemPackages = [alejandra.defaultPackage.${system}];
              }
              # Import your other modules here
              # ./path/to/my/module.nix
              # ...
            ];
          };
        };
      };
    }

Do I need to configure anything?

  • No.

Discussion

Cool libraries

Alternatives

See why Alejandra was created and a comparison between alternatives here.

Alternatively, checkout the code examples of the different formatters here.

Versioning

We use semver to version Alejandra.

Our public API consists of:

  • The formatting rules (a.k.a. the style).
  • The CLI tool ($ alejandra), command line flags, positional arguments, exit codes, and stdout.

Changelog

Please read: CHANGELOG.

Contributors

The following people have helped improving Alejandra.

Thank you ❀️

Footnotes

Footnotes

  1. Running on a machine with:

    • CPU: 4 physical, 4 logical, 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
    • MHz: from 400 to 4700 MHz
    • BogoMips: 5606.40
    • Cache L3: 12 MiB

    Using:

    # x86_64-unknown-linux-gnu
    $ time alejandra --threads $threads /path/to/nixpkgs
    

    Results:

    $threads Seconds
    1 45
    2 25
    4 14
    ↩
  2. The methodology to claim this is:

    1. Checkout Nixpkgs and run:

      $ nix-env -qaf . --drv-path --xml > before
      
    2. Now format with Alejandra and run:

      $ nix-env -qaf . --drv-path --xml > after
      

    As of 2022-06-22, there are 41 differences in a set of 38109 derivations because of things like this:

    goDeps = ./deps.nix;
    

    Since ./deps.nix was also formatted you get a semantical difference.

    This is something that should be solved on Nixpkgs and not a bug in Alejandra. For example:

    ↩

alejandra's People

Contributors

9999years avatar blaggacao avatar connorbaker avatar dependabot[bot] avatar kamadorueda avatar l0b0 avatar loicreynier avatar mic92 avatar mkenigs avatar n8henrie avatar piegamesde avatar pyrox0 avatar rehno-lindeque avatar sciencentistguy avatar tazjin avatar tobiasbora avatar tomberek avatar usersv4 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

alejandra's Issues

Create IDE or editors integrations

Create plugins/integrations, easy ways for people to use their tool in their natural habitat (vscode, vim, emacs, even cat?, etc)

Using the statically linked versions of the tool helps making deployment straightforward

Let's version the code for that in this repository as well, so we don't confuse people where to report issues to

Overly-aggressive parenthesis expansion

Here is an example that makes things less readable:

-      nixos-config = (pkgs.nixos [
-        ({ pkgs, ... }: { nix.package = pkgs.nixUnstable; })
-      ]);
+      nixos-config = (
+        pkgs.nixos [
+          (
+            { pkgs
+            , ...
+            }:
+            { nix.package = pkgs.nixUnstable; }
+          )
+        ]
+      );

Alejandra could actually remove the first set of parenthesis as they are not needed.

Why does it break the second line multi-line? In general, it feels like function arguments should be on the same line as the parenthesis but I'm not sure.

aarch64-darwin build failure

Using Nix v2.5.1 on Monterey, aarch64-darwin, I tried both install commands given in the README (nix-env -ivA aarch64-darwin -f https://github.com/kamadorueda/alejandra/tarball/main at revision ea6e3bf, and nix profile install github:kamadorueda/alejandra). The complete log ends with:

       last 10 log lines:
       > libredirect.c:147:27: warning: second argument to 'va_arg' is of promotable type 'mode_t' (aka 'unsigned short'); this va_arg has undefined behavior because arguments will be promoted to 'int' [-Wvarargs]
       >         mode = va_arg(ap, mode_t);
       >                           ^~~~~~
       > /nix/store/n9rwf7h1sids29i0zl077jq8sd9nssp3-clang-13.0.0-lib/lib/clang/13.0.0/include/stdarg.h:19:50: note: expanded from macro 'va_arg'
       > #define va_arg(ap, type)    __builtin_va_arg(ap, type)
       >                                                  ^~~~
       > 4 warnings generated.
       > ld: warning: ignoring file /private/tmp/nix-build-libredirect-0.drv-0/libredirect-e9331c.o, building for macOS-arm64 but attempting to link with file built for macOS-arm64
       > fatal error: /nix/store/diz9chv9r9m3pv9rzi7g3p2iq8vgsmr3-cctools-binutils-darwin-949.0.1/bin/lipo: /private/tmp/nix-build-libredirect-0.drv-0/-771885.out and /private/tmp/nix-build-libredirect-0.drv-0/-cc083d.out have the same architectures (arm64) and can't be in the same fat output file
       > clang-13: error: lipo command failed with exit code 1 (use -v to see invocation)
       For full logs, run 'nix log /nix/store/w4h3cdgb32bl9znyx5nf1rf83mbp8z8m-libredirect-0.drv'.

I bisected the repo down to 446a7b0, which it turns out is where support for aarch64-darwin was first added, and even that revision fails to build for the same reason.

How were you verifying that aarch64-darwin builds? (That is, do you have evidence that it does?)

Use semver

Self explanatory, let's define the formatting rules as the public API, and version that with semver

This gives people the guarantee that we'll take care of stability, and that we have reached a maturity state of production ready

Help us get coverage to 100%

Enter the development environment and run:

$ cargo tarpaulin -o html

Now open ./tarpaulin-report.html in your web browser, for instance:

$ google-chrome-stable tarpaulin-report.html

And you'll see a detailed coverage of each file, line by line of the portions of the code that are exercised during the tests (green) or not (red), there may also be cases of dead code:

image

Help us writing test cases that make everything look green

The idea is to get to 100% coverage, which help a lot avoiding regressions, and improving the reliability of the tool

Max line width adherance appears inconsistent

Hi, apologies if there is some pattern to this that I'm not quite seeing, but looking at some reformatting patterns in my system flake:

-  imports =
-    [ ./hardware-configuration.nix ../bluetooth.nix ../power.nix ../wifi.nix ];
+  imports = [ ./hardware-configuration.nix ../bluetooth.nix ../power.nix ../wifi.nix ];

79 -> 87 characters. While I find this nicer to look at, this breaks the max width.

Another, more complex example is:

-  nixpkgs.config.allowUnfreePredicate = pkg:
-    builtins.elem (pkgs.lib.getName pkg) [ "steam-original" ];
+  nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "steam-original" ];

On the other hand, I find this elsewhere:

-    fonts = with pkgs; [ hack-font noto-fonts noto-fonts-cjk noto-fonts-emoji ];
+    fonts =
+      with pkgs; [ hack-font noto-fonts noto-fonts-cjk noto-fonts-emoji ];

80 -> 74 characters. The fact that this means 79 is the de-facto max line width aside, it seems strange that this would be formatted to strictly adhere to that limit, whereas the former examples are not.

I saw #51, but b918b9b produces the same output, and the test case seems unrelated to my untrained eyes. Sometimes abandoning strict max-line-width certainly makes sense, but it would be great if the behavior was a bit more obviously predictable :)

Cachix: consider a cachix push on master's head

Projects that want to track WIP-alerjandra would benefit from a cachix action to provide a readily built cache hit on new commits.

Otherwise, it can get a bit (time-)expensive to suffer the frequent rebuilds.

Luxury problem.

Don’t remove single empty lines

This one requires a big change in the architecture

I think the sane place to apply this rule would be to attribute sets, lists and let-in expressions only

Indentation on function calls

lib.mkOption {
  type = types.nullOr types.str;
  default = null;
}

instead of

lib.mkOption
  {
    type = types.nullOr types.str;
    default = null;
  }

Offer pre-built binaries

I want to make it super-accessible to use Alejandra, let's show in the README how to curl and get Alejandra running right away

Inline comments

alejandra formats all my inline comments to be on the next line,

 accepted_roles = [
+        # only these roles are ok
         "devtools"
         "workstation"
         "remote"
@@ -90,8 +96,10 @@
         "mqtt-server"
         "mqtt-client"
-        "zpool-watcher" # where our watcher runs
-        "zpool-systems" # the systems being watched
+        "zpool-watcher"
+        # where our watcher runs
+        "zpool-systems"
+        # the systems being watched
         "virtualbox"

that obscures the meaning :(.

Always expand if/then/else

if n == len 
then value 
else { ${ elemAt attrPath n } = atDepth (n + 1); }

Instead of

if n == len then value else { ${ elemAt attrPath n } = atDepth (n + 1); }

Expand let bindings if more than 1 bind is done

more than 1 binding becomes unreadable:

diff --git a/lib/fixed-points.nix b/lib/fixed-points.nix
index 6898b24c820..1c9664f6801 100644
--- a/lib/fixed-points.nix
+++ b/lib/fixed-points.nix
@@ -71,7 +71,12 @@ rec {
   # into one where changes made in the first are available in the
   # 'super' of the second
   composeExtensions =
-    f: g: final: prev: let fApplied = f final prev; prev' = prev // fApplied; in fApplied // g final prev';
+    f: g: final: prev:
+    let
+      fApplied = f final prev;
+      prev' = prev // fApplied;
+    in
+      fApplied // g final prev';
   # Compose several extending functions of the type expected by 'extends' into
   # one where changes made in preceding functions are made available to
   # subsequent ones.

Enforcing spaces in containers?

I see that you once tried changing the behavior of nixpkgs-fmt to no space (nix-community/nixpkgs-fmt#280).
Any reason why you changed your mind?

In the equivalent discussion on nixpkgs-fmt, it seemed like everyone kind of agreed that it doesn't impact readability and just results in more horizontal space taken. Also it was shown that most code in nixpkgs uses no space (Not that I think this is important, but at least we know that not many people there are going to complain about spaces being removed).

Indent let bindings

let
  pattern = if builtins.isString elem then { system = elem; } else { parsed = elem; };
in
  lib.matchAttrs pattern platform;

instead of

let
  pattern = if builtins.isString elem then { system = elem; } else { parsed = elem; };
in
lib.matchAttrs pattern platform;

Unformatted valid Nix code

Hi, I found a bug related to nix-community/rnix-parser#23.

Input

let b = a: a; "or" = 5; in [b or]

Nix repl

[ 5 ]

Alejandra Output

Error: unexpected TOKEN_SQUARE_B_CLOSE at 32..33, wanted any of [TOKEN_PAREN_OPEN, TOKEN_REC, TOKEN_CURLY_B_OPEN, TOKEN_SQUARE_B_OPEN, TOKEN_DYNAMIC_START, TOKEN_STRING_START, TOKEN_IDENT], at: stdin

The stdout contains the input code, which is not formatted.

Expected output

let
  b = a: a;
  "or" = 5;
in [b or]

`cargo test` - fix underway

❯ cargo test
error: failed to parse manifest at `/home/blaggacao/src/github.com/kamadorueda/alejandra/Cargo.toml`

Caused by:
  feature `edition2021` is required

  this Cargo does not support nightly features, but if you
  switch to nightly channel you can add
  `cargo-features = ["edition2021"]` to enable this feature

expand attr-set if it contains more than one element

Compact containers (like lists and attr-sets) are nice,
but they can be unreadable when they contain more than one element

Below the proposed change:

--- a/nixos-modules/editor/default.nix
+++ b/nixos-modules/editor/default.nix
@@ -44,9 +44,18 @@ let
     "[python]"."editor.tabSize" = 4;
     "[rust]"."editor.tabSize" = 4;
     "customLocalFormatters.formatters" = [
-      { command = "clang-format --sort-includes --style=microsoft"; languages = [ "cpp" ]; }
-      { command = "${ nixpkgs.jq }/bin/jq -S"; languages = [ "json" "jsonc" ]; }
-      { command = alejandra.outputs.defaultApp.${ nixpkgs.system }.program; languages = [ "nix" ]; }
+      {
+        command = "clang-format --sort-includes --style=microsoft";
+        languages = [ "cpp" ];
+      }
+      {
+        command = "${ nixpkgs.jq }/bin/jq -S";
+        languages = [ "json" "jsonc" ];
+      }
+      {
+        command = alejandra.outputs.defaultApp.${ nixpkgs.system }.program;
+        languages = [ "nix" ];
+      }

Collapse nested lambdas

--- a/lib/lists.nix
+++ b/lib/lists.nix
@@ -48,9 +48,7 @@ in
        => "2345a"
      */
     foldr =
-      op:
-      nul:
-      list:
+      op: nul: list:
       let
         len = length list;
         fold' =

Do not indent after second binary operator

 267   β”‚             docOption =
 268   β”‚               rec {
 269   β”‚                 loc = opt.loc;
 270   β”‚                 name = showOption opt.loc;
 271   β”‚                 description = opt.description or null;
 272   β”‚                 declarations = filter (x: x != unknownModule) opt.declarations;
 273   β”‚                 internal = opt.internal or false;
 274   β”‚                 visible =
 275   β”‚                   if (opt ? visible && opt.visible == "shallow")
 276   β”‚                   then true
 277   β”‚                   else opt.visible or true;
 278   β”‚                 readOnly = opt.readOnly or false;
 279   β”‚                 type = opt.type.description or null;
 280   β”‚               }
 281   β”‚               // optionalAttrs (opt ? example) { example = scrubOptionValue opt.example; }
 282   β”‚               // optionalAttrs (opt ? default) { default = scrubOptionValue opt.default; }
 283   β”‚               // optionalAttrs (opt ? defaultText) { default = opt.defaultText; }
 284   β”‚               // optionalAttrs
 285   β”‚                 (opt ? relatedPackages && opt.relatedPackages != null)
 286   β”‚                 { inherit (opt) relatedPackages; };

Instead of:

 267   β”‚             docOption =
 268   β”‚               rec {
 269   β”‚                 loc = opt.loc;
 270   β”‚                 name = showOption opt.loc;
 271   β”‚                 description = opt.description or null;
 272   β”‚                 declarations = filter (x: x != unknownModule) opt.declarations;
 273   β”‚                 internal = opt.internal or false;
 274   β”‚                 visible =
 275   β”‚                   if (opt ? visible && opt.visible == "shallow")
 276   β”‚                   then true
 277   β”‚                   else opt.visible or true;
 278   β”‚                 readOnly = opt.readOnly or false;
 279   β”‚                 type = opt.type.description or null;
 280   β”‚               }
 281   β”‚                 // optionalAttrs (opt ? example) { example = scrubOptionValue opt.example; }
 282   β”‚                 // optionalAttrs (opt ? default) { default = scrubOptionValue opt.default; }
 283   β”‚                 // optionalAttrs (opt ? defaultText) { default = opt.defaultText; }
 284   β”‚                 // optionalAttrs
 285   β”‚                   (opt ? relatedPackages && opt.relatedPackages != null)
 286   β”‚                   { inherit (opt) relatedPackages; };

same for select nodes (a.b.c)

Long quotes on the same line

postPatch = ''
  ln -s ${ doc-support } ./doc-support/result
'';

instead of

postPatch =
  ''
  ln -s ${ doc-support } ./doc-support/result
  '';

alejandra introduced a syntax error

Hi! We're trying a bunch of different formatters for the TVL depot (nixfmt, nixpkgs-fmt). Currently none of them really do what we want, but I digress ...

We tried alejandra and it seems to have introduced errors that would break our build graph evaluation.

In fact the file surfaced in the error seems to have several pathological cases in its diff. There's a syntax error introduced somewhere (I didn't track it down exactly, but I think it's related to dynamic attribute set keys), but there's also some behaviour such as detaching comments from the correct list items.

Cheers

Strip unnecessary parenthesis

In the following case, the parens are not necessary:

+  {
+    binPath =
+      with pkgs;
+      makeBinPath (
+        [
+          rsync
+          util-linux
+        ]
+      );
+  }

after a key-value they can also be deleted, etc

visual change on multi-line string

Here is a minimal repo of the issue:

{
  fetch_builtin-url =
    ''[name] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
        $ niv modify name -a type=file -a builtin=true'';
}

After formatting:

--- a/repro.nix
+++ b/repro.nix
@@ -1,5 +1,5 @@
 {
   fetch_builtin-url =
-    ''[name] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
-        $ niv modify name -a type=file -a builtin=true'';
+    ''      [name] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
+              $ niv modify name -a type=file -a builtin=true'';
 }

As you can see, the multi-line string has gained a few whitespaces in front, changing the content of the Nix value.

This is probably related to nix-community/rnix-parser#71

No whitespace around dynamics

diff --git a/tests/cases/dynamic/out b/tests/cases/dynamic/out
index 6f74b7d..5eb8cec 100644
--- a/tests/cases/dynamic/out
+++ b/tests/cases/dynamic/out
@@ -8,7 +8,7 @@ a
     /*
      d
      */
-    e.${ f }
+    e.${f}
   }
   /*
    g

Following the crowd != Uncompromising

For the projects I'm maintaining, I'm looking for a code formatter.
The reason why formatters like nixpkgs-fmt aren't optimal for me is because they try to implement the predominant style in nixpkgs. The problem with this is, that some things in nixpkgs are just bad and therefore following nixpkgs style comes with compromises.

When I first encountered alejandra - The Uncompromising Nix Code Formatter my hopes were high. Sadly I see that the readme has been updated a few days ago and now contains the statement:
We optimize for the wisdom of the crowd, which comes in big part from the 2.3 million lines of code of [Nixpkgs](https://github.com/NixOS/nixpkgs).

This stands in opposition to the title of the project itself. If alejandra actually was uncompromising, it shouldn't be important what people have been doing before.

I understand that alejandra tries to be a viable candidate for RFC101, but that comes with compromises.

If I go through the issues here, I see comments indicating that minimizing the initial diff on nixpkgs could be a goal of alejandra (#60 (comment)).

I'm not intending to tell anyone here what to do. RFC101 is definitely a goal worth achieving, but it is not uncompromising and therefore would make the formatter most likely less interesting for my projects where I don't care about minimizing the diff and prefer the most practical style instead.

let .. in balancing

Consider the following starting example:

{
  a = 1;
  b = 2;
  #...
  z = 26;
}

Now let's say the code is evolving and I need to add a let .. in block on top:

let
  double = x: x+x;
in

In the current version, this pushes the whole attrset to the right, causing a lot of whitespace formatting diff.

I think the question is; do you prefer a balanced let <statement> in <body>, or to minimize the diff when the code changes? Both are valid choices IMO, it's just a matter of what you value most.

Leading commas are unhandy

I know that leading commas seem to be a standard in haskell and somehow everyone does it, but actually it's unhandy when editing code.

Explanation of why leading commas negatively impact editing experience is already given in the equivalent issue on nixpkgs-fmt: nix-community/nixpkgs-fmt#248 (comment)
To summarize what has already been said:

  • leading commas make argument lists inconsistent. The first line starts with { and subsiding ones with ,.
  • it makes re-ordering arguments more complicated, one cannot just shift lines around without braking stuff.
  • inserting a new argument in front of the first one is more complicated, as one cannot just duplicate the first line to then edit it.

In the other thread, there were a lot of opposing opinions to what I'm saying, but if we are having this discussion on a scientific basis, any opposing argument to this should either contain an explanation of why my statements are wrong or irrelevant, or explain disadvantages of trailing commas.
I am still waiting for such a response.

I think if we want to achieve the best possible result, we need to leave personal preferences out of this as well as statements like but this is what everyone does.

Ignore files by pattern

It would be great if alejandra had the ability to provide ignore patterns to exclude some files / drectories from formatting.

In my projects I sometimes copy nix files from other projects. Those files should not be formatted, as this makes it harder to diff against future upstream changes.
Another case where formatting might not be wanted is for automatically generated nix code.

Another feature that I've seen with other formatters is to exclude code snippets from being formatted via a comment inside the code.
(Just mentioning this here, but I currently I do not have a use case for it)

As a reference:
Prettier's docs on excluding code

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.