Git Repositories

explain-git-with-d3
authorCyrille Pontvieux <jrd@enialis.net>
Wed, 18 May 2016 21:27:48 +0000 (23:27 +0200)
committerCyrille Pontvieux <jrd@enialis.net>
Wed, 18 May 2016 21:27:48 +0000 (23:27 +0200)
14 files changed:
explain-git-with-d3/LICENSE.md [new file with mode: 0644]
explain-git-with-d3/README.md [new file with mode: 0644]
explain-git-with-d3/css/1140.css [new file with mode: 0644]
explain-git-with-d3/css/explaingit.css [new file with mode: 0644]
explain-git-with-d3/css/normalize.css [new file with mode: 0644]
explain-git-with-d3/images/forkme_right_red_aa0000.png [new file with mode: 0644]
explain-git-with-d3/images/prompt.gif [new file with mode: 0644]
explain-git-with-d3/index.html [new file with mode: 0644]
explain-git-with-d3/js/controlbox.js [new file with mode: 0644]
explain-git-with-d3/js/explaingit.js [new file with mode: 0644]
explain-git-with-d3/js/historyview.js [new file with mode: 0644]
explain-git-with-d3/js/main.js [new file with mode: 0644]
explain-git-with-d3/js/require.min.js [new file with mode: 0644]
explain-git-with-d3/memtest.html [new file with mode: 0644]

diff --git a/explain-git-with-d3/LICENSE.md b/explain-git-with-d3/LICENSE.md
new file mode 100644 (file)
index 0000000..ce1377e
--- /dev/null
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Wei Wang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/explain-git-with-d3/README.md b/explain-git-with-d3/README.md
new file mode 100644 (file)
index 0000000..18564f4
--- /dev/null
@@ -0,0 +1,13 @@
+explain-git-with-d3
+===================
+
+Use D3 to visualize simple git branching operations.
+
+This simple project is designed to help people understand some basic git concepts visually.
+
+This is my first attempt at using both SVG and D3. I hope it is helpful to you.
+
+I upload the contents of this repository via FTP every once in a while to: http://www.wei-wang.com/ExplainGitWithD3/
+so you can just visit that site to use an (almost) up to date version.
+
+UPDATE: the page can now also be accessed via: http://onlywei.github.io/explain-git-with-d3/
diff --git a/explain-git-with-d3/css/1140.css b/explain-git-with-d3/css/1140.css
new file mode 100644 (file)
index 0000000..b8d6fa2
--- /dev/null
@@ -0,0 +1,130 @@
+/* CSS Resets */
+
+html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,address,cite,code,del,dfn,em,img,ins,q,small,strong,sub,sup,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{border:0;margin:0;padding:0}article,aside,figure,figure img,figcaption,hgroup,footer,header,nav,section,video,object{display:block}a img{border:0}figure{position:relative}figure img{width:100%}
+
+
+/* ==================================================================================================================== */
+/* ! The 1140px Grid V2 by Andy Taylor \ http://cssgrid.net \ http://www.twitter.com/andytlr \ http://www.andytlr.com   */
+/* ==================================================================================================================== */
+
+.container {
+padding-left: 20px;
+padding-right: 20px;
+}
+
+.row {
+width: 100%;
+max-width: 1140px;
+min-width: 755px;
+margin: 0 auto;
+overflow: hidden;
+}
+
+.onecol, .twocol, .threecol, .fourcol, .fivecol, .sixcol, .sevencol, .eightcol, .ninecol, .tencol, .elevencol {
+margin-right: 3.8%;
+float: left;
+min-height: 1px;
+}
+
+.row .onecol {
+width: 4.85%;
+}
+
+.row .twocol {
+width: 13.45%;
+}
+
+.row .threecol {
+width: 22.05%;
+}
+
+.row .fourcol {
+width: 30.75%;
+}
+
+.row .fivecol {
+width: 39.45%;
+}
+
+.row .sixcol {
+width: 48%;
+}
+
+.row .sevencol {
+width: 56.75%;
+}
+
+.row .eightcol {
+width: 65.4%;
+}
+
+.row .ninecol {
+width: 74.05%;
+}
+
+.row .tencol {
+width: 82.7%;
+}
+
+.row .elevencol {
+width: 91.35%;
+}
+
+.row .twelvecol {
+width: 100%;
+float: left;
+}
+
+.last {
+margin-right: 0px;
+}
+
+img, object, embed {
+max-width: 100%;
+}
+
+img {
+       height: auto;
+}
+
+
+/* Smaller screens */
+
+@media only screen and (max-width: 1023px) {
+
+       body {
+       font-size: 0.8em;
+       line-height: 1.5em;
+       }
+       
+       }
+
+
+/* Mobile */
+
+@media handheld, only screen and (max-width: 767px) {
+
+       body {
+       font-size: 16px;
+       -webkit-text-size-adjust: none;
+       }
+       
+       .row, body, .container {
+       width: 100%;
+       min-width: 0;
+       margin-left: 0px;
+       margin-right: 0px;
+       padding-left: 0px;
+       padding-right: 0px;
+       }
+       
+       .row .onecol, .row .twocol, .row .threecol, .row .fourcol, .row .fivecol, .row .sixcol, .row .sevencol, .row .eightcol, .row .ninecol, .row .tencol, .row .elevencol, .row .twelvecol {
+       width: auto;
+       float: none;
+       margin-left: 0px;
+       margin-right: 0px;
+       padding-left: 20px;
+       padding-right: 20px;
+       }
+
+}
\ No newline at end of file
diff --git a/explain-git-with-d3/css/explaingit.css b/explain-git-with-d3/css/explaingit.css
new file mode 100644 (file)
index 0000000..cbae11b
--- /dev/null
@@ -0,0 +1,261 @@
+/* styles */
+
+body, html {
+    height: 100%;
+}
+
+.intro p, .concept-container p {
+    padding-top: 10px;
+}
+
+a.openswitch {
+    display: block;
+}
+
+a.openswitch.selected {
+    font-weight: bold;
+}
+
+.command-list, .example-list {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 10px 0;
+    border-bottom: 2px dashed #888;
+    border-top: 2px dashed #888;
+    background-color: #EEE;
+}
+
+.command-list a.openswitch {
+    font-family: Courier New;
+}
+
+.concept-area {
+    padding-bottom: 20px;
+}
+
+.concept-container {
+    display: none;
+}
+
+.playground-container {
+    margin-top: 20px;
+    position: relative;
+}
+
+span.cmd {
+    background-color: #222222;
+    color: #FFFFFF;
+    font-family: Courier New;
+    padding: 0 0.2em;
+}
+
+.svg-container {
+    margin-left:250px;
+    display: block;
+    overflow: auto;
+    border: 1px dotted #AAA;
+}
+
+.svg-container.remote-container {
+    position: absolute;
+    top: 0px; right: 0px;
+    background-color: #EFF1FF;
+    border-left: 1px dotted #AAA;
+    border-bottom: 1px dotted #AAA;
+}
+
+#ExplainGitZen-Container {
+    position: absolute;
+    top: 0; bottom: 0; right: 0; left: 0;
+}
+
+#ExplainGitZen-Container .svg-container {
+    display: inline-block;
+    border: 1px dotted #AAA;
+    position: absolute;
+    top: 0; bottom: 0; right: 0; left: 250px;
+    margin-left: 0;
+}
+
+#ExplainGitZen-Container .svg-container.remote-container {
+    position: absolute;
+    top: 0px; right: 0px; left: auto; bottom: auto;
+    background-color: #EFF1FF;
+    border-left: 1px dotted #AAA;
+    border-bottom: 1px dotted #AAA;
+}
+
+#ExplainGitZen-Container .playground-container {
+    position: absolute;
+    top: 0; bottom: 20px; right: 20px; left: 20px;
+}
+
+.remote-name-display {
+    font-weight: bold;
+    text-align: right;
+}
+
+.control-box {
+    display: inline-block;
+    position: absolute;
+    top: 0px; bottom: 0;
+    width: 250px;
+    vertical-align: bottom;
+    background-color: #000;
+    border: 1px dotted #AAA;
+}
+
+.control-box button {
+    font-family: Courier New;
+    font-size: 12px;
+    margin-right: 5px;
+    margin-bottom: 5px;
+}
+
+.control-box .log {
+    overflow-y: auto;
+    position: absolute;
+    top: 0px; bottom: 20px; left: 0; right: 0;
+    border-bottom: 1px solid #AAA;
+}
+
+.control-box .log,
+.control-box input[type="text"] {
+    font-family: Courier New;
+    font-size: 14px;
+}
+
+.control-box .log .command-entry {
+    padding-left: 15px;
+    color: #FFF;
+    line-height: 14px;
+    background: url(../images/prompt.gif) no-repeat left center transparent;
+}
+
+.control-box input[type="text"] {
+    position: absolute;
+    bottom: 0; 
+    padding-left: 15px;
+    color: #FFF;
+    line-height: 14px;
+    background: url(../images/prompt.gif) no-repeat left center transparent;
+}
+
+.control-box .log .info,
+.control-box .log .error {
+    font-size: 12px;
+    padding: 5px;
+}
+
+.control-box .log .info {
+    color: #FFC;
+}
+
+.control-box .log .error {
+    color: #FCC;
+}
+
+.control-box input[type="text"] {
+    width: 235px;
+    border: none;
+}
+
+circle.commit {
+    fill: #EEEEEE;
+    stroke: #888888;
+    stroke-width: 3;
+}
+
+circle.commit.checked-out {
+    fill: #CCFFCC !important;
+    stroke: #339900;
+}
+
+circle.commit.merge-commit {
+    stroke: #663300;
+    fill: #FFFFCC;
+}
+
+circle.commit.reverted {
+    fill: #FFC;
+    stroke: #933;
+}
+
+circle.commit.rebased {
+    stroke: #3300CC;
+    fill: #CCCCFF;
+}
+
+circle.commit.branchless {
+    fill: #FEFEFE;
+    stroke: #DDD;
+}
+
+.commit-pointer {
+    stroke: #666;
+    stroke-width: 4;
+}
+
+.merge-pointer {
+    stroke: #663300;
+    stroke-width: 4;
+}
+
+.commit-pointer.branchless,
+.merge-pointer.branchless {
+    stroke: #DDD;
+    stroke-width: 2;
+}
+
+text.id-label {
+       text-anchor: middle;
+       font-family: Courier New;
+       font-weight: bolder;
+       fill: #666;
+       font-size: 10px;
+}
+
+text.message-label {
+    text-anchor: middle;
+    font-family: Courier New;
+    fill: #666;
+    font-size: 10px;
+}
+
+g.branch-tag > rect {
+       fill: #FFCC66;
+       stroke: #CC9900;
+       stroke-width: 2;
+}
+
+g.branch-tag.git-tag > rect {
+       fill: #7FC9FF;
+       stroke: #0026FF;
+}
+
+g.branch-tag.remote-branch > rect {
+       fill: #CCC;
+       stroke: #888;
+}
+
+g.branch-tag > text {
+       text-anchor: middle;
+       fill: #000;
+       font-size: 11px;
+       font-family: Arial;
+}
+
+g.head-tag > rect {
+       fill: #CCFFCC;
+       stroke: #339900;
+       stroke-width: 2;
+}
+
+g.head-tag > text {
+       text-anchor: middle;
+       fill: #000;
+       font-size: 11px;
+       font-family: Arial;
+       font-weight: bold;
+       text-transform: uppercase;
+}
diff --git a/explain-git-with-d3/css/normalize.css b/explain-git-with-d3/css/normalize.css
new file mode 100644 (file)
index 0000000..a9c6f52
--- /dev/null
@@ -0,0 +1,396 @@
+/*! normalize.css v2.1.0 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+   HTML5 display definitions
+   ========================================================================== */
+
+/**
+ * Correct `block` display not defined in IE 8/9.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+    display: block;
+}
+
+/**
+ * Correct `inline-block` display not defined in IE 8/9.
+ */
+
+audio,
+canvas,
+video {
+    display: inline-block;
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+    display: none;
+    height: 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+[hidden] {
+    display: none;
+}
+
+/* ==========================================================================
+   Base
+   ========================================================================== */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ *    user zoom.
+ */
+
+html {
+    font-family: sans-serif; /* 1 */
+    -webkit-text-size-adjust: 100%; /* 2 */
+    -ms-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+    margin: 0;
+}
+
+/* ==========================================================================
+   Links
+   ========================================================================== */
+
+/**
+ * Address `outline` inconsistency between Chrome and other browsers.
+ */
+
+a:focus {
+    outline: thin dotted;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+    outline: 0;
+}
+
+/* ==========================================================================
+   Typography
+   ========================================================================== */
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari 5, and Chrome.
+ */
+
+h1 {
+    font-size: 2em;
+    margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9, Safari 5, and Chrome.
+ */
+
+abbr[title] {
+    border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
+ */
+
+b,
+strong {
+    font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari 5 and Chrome.
+ */
+
+dfn {
+    font-style: italic;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+    -moz-box-sizing: content-box;
+    box-sizing: content-box;
+    height: 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+    background: #ff0;
+    color: #000;
+}
+
+/**
+ * Correct font family set oddly in Safari 5 and Chrome.
+ */
+
+code,
+kbd,
+pre,
+samp {
+    font-family: monospace, serif;
+    font-size: 1em;
+}
+
+/**
+ * Improve readability of pre-formatted text in all browsers.
+ */
+
+pre {
+    white-space: pre-wrap;
+}
+
+/**
+ * Set consistent quote types.
+ */
+
+q {
+    quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+    font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline;
+}
+
+sup {
+    top: -0.5em;
+}
+
+sub {
+    bottom: -0.25em;
+}
+
+/* ==========================================================================
+   Embedded content
+   ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9.
+ */
+
+img {
+    border: 0;
+}
+
+/**
+ * Correct overflow displayed oddly in IE 9.
+ */
+
+svg:not(:root) {
+    overflow: hidden;
+}
+
+/* ==========================================================================
+   Figures
+   ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari 5.
+ */
+
+figure {
+    margin: 0;
+}
+
+/* ==========================================================================
+   Forms
+   ========================================================================== */
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+    border: 1px solid #c0c0c0;
+    margin: 0 2px;
+    padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+    border: 0; /* 1 */
+    padding: 0; /* 2 */
+}
+
+/**
+ * 1. Correct font family not being inherited in all browsers.
+ * 2. Correct font size not being inherited in all browsers.
+ * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
+ */
+
+button,
+input,
+select,
+textarea {
+    font-family: inherit; /* 1 */
+    font-size: 100%; /* 2 */
+    margin: 0; /* 3 */
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+button,
+input {
+    line-height: normal;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
+ * Correct `select` style inheritance in Firefox 4+ and Opera.
+ */
+
+button,
+select {
+    text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ *    and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ *    `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+    -webkit-appearance: button; /* 2 */
+    cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+    cursor: default;
+}
+
+/**
+ * 1. Address box sizing set to `content-box` in IE 8/9.
+ * 2. Remove excess padding in IE 8/9.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+    box-sizing: border-box; /* 1 */
+    padding: 0; /* 2 */
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
+ *    (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+    -webkit-appearance: textfield; /* 1 */
+    -moz-box-sizing: content-box;
+    -webkit-box-sizing: content-box; /* 2 */
+    box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+    -webkit-appearance: none;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+    border: 0;
+    padding: 0;
+}
+
+/**
+ * 1. Remove default vertical scrollbar in IE 8/9.
+ * 2. Improve readability and alignment in all browsers.
+ */
+
+textarea {
+    overflow: auto; /* 1 */
+    vertical-align: top; /* 2 */
+}
+
+/* ==========================================================================
+   Tables
+   ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
diff --git a/explain-git-with-d3/images/forkme_right_red_aa0000.png b/explain-git-with-d3/images/forkme_right_red_aa0000.png
new file mode 100644 (file)
index 0000000..1e19c21
Binary files /dev/null and b/explain-git-with-d3/images/forkme_right_red_aa0000.png differ
diff --git a/explain-git-with-d3/images/prompt.gif b/explain-git-with-d3/images/prompt.gif
new file mode 100644 (file)
index 0000000..ada0b9c
Binary files /dev/null and b/explain-git-with-d3/images/prompt.gif differ
diff --git a/explain-git-with-d3/index.html b/explain-git-with-d3/index.html
new file mode 100644 (file)
index 0000000..e2d018b
--- /dev/null
@@ -0,0 +1,560 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+<title>Explain Git with D3</title>
+<script data-main="js/main" src="js/require.min.js"></script>
+<link rel="stylesheet" href="css/normalize.css">
+<link rel="stylesheet" href="css/1140.css">
+<link rel="stylesheet" href="css/explaingit.css">
+</head>
+<body>
+<a href="https://github.com/onlywei/explain-git-with-d3" id="fork-me">
+  <img style="position: absolute; top: 0; right: 0; border: 0;"
+    src="images/forkme_right_red_aa0000.png"
+    alt="Fork me on GitHub">
+</a>
+<div class="container">
+  <div class="row intro">
+    <div class="twelvecol">
+      <h1>Visualizing Git Concepts with D3</h1>
+      <p>
+        This website is designed to help you understand some basic git concepts visually.
+        This is my first attempt at using both SVG and D3. I hope it is helpful to you.
+      </p>
+      <p>
+        Adding/staging your files for commit will not be covered by this site. In all sandbox playgrounds
+        on this site, just pretend that you always have files staged and ready to commit at all times.
+        If you need a refresher on how to add or stage files for commit, please read
+        <a href="http://git-scm.com/book/en/Git-Basics-Recording-Changes-to-the-Repository">Git Basics</a>.
+      </p>
+      <p>
+        Sandboxes are split by specific git commands, listed below.
+      </p>
+    </div>
+  </div>
+  <div class="row command-list">
+    <div class="twocol">
+      <h4>Basic Commands</h3>
+      <a id="open-commit" class="openswitch" href="#commit">git commit</a>
+      <a id="open-branch" class="openswitch" href="#branch">git branch</a>
+    </div>
+    <div class="twocol">
+      <h4>&nbsp;</h4>
+      <a id="open-checkout" class="openswitch" href="#checkout">git checkout</a>
+      <a id="open-checkout-b" class="openswitch" href="#checkout-b">git checkout -b</a>
+    </div>
+    <div class="twocol">
+      <h4>Undo Commits</h4>
+      <a id="open-reset" class="openswitch" href="#reset">git reset</a>
+      <a id="open-revert" class="openswitch" href="#revert">git revert</a>
+    </div>
+    <div class="twocol">
+      <h4>Combine Branches</h4>
+      <a id="open-merge" class="openswitch" href="#merge">git merge</a>
+      <a id="open-rebase" class="openswitch" href="#rebase">git rebase</a>
+    </div>
+    <div class="twocol">
+      <h4>Remote Server</h4>
+      <a id="open-fetch" class="openswitch" href="#fetch">git fetch</a>
+      <a id="open-pull" class="openswitch" href="#pull">git pull</a>
+    </div>
+    <div class="twocol last">
+      <h4>&nbsp;</h4>
+      <a id="open-push" class="openswitch" href="#push">git push</a>
+      <a id="open-tag" class="openswitch" href="#tag">git tag</a>
+    </div>
+  </div>
+  <div class="row concept-area">
+    <div id="ExplainGitCommit-Container" class="twelvecol concept-container">
+      <p>
+        We are going to skip instructing you on how to add your files for commit in this explanation.
+        Let's assume you already know how to do that. If you don't, go read some other tutorials.
+      </p>
+      <p>
+        Pretend that you already have your files staged for commit and enter <span class="cmd">git commit</span>
+        as many times as you like in the terminal box.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitTag-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git tag name</span> will create a new tag named "name".
+        Creating tags just creates a new tag pointing to the currently checked out commit.
+      </p>
+      <p>
+        Tags can be deleted using the command <span class="cmd">git tag -d name</span> (coming soon).
+      </p>
+      <p>
+        Type <span class="cmd">git commit</span> and <span class="cmd">git tag</span> commands
+        to your hearts desire until you understand this concept.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitBranch-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git branch name</span> will create a new branch named "name".
+        Creating branches just creates a new tag pointing to the currently checked out commit.
+      </p>
+      <p>
+        Branches can be deleted using the command <span class="cmd">git branch -d name</span>.
+      </p>
+      <p>
+        Type <span class="cmd">git commit</span> and <span class="cmd">git branch</span> commands
+        to your hearts desire until you understand this concept.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitCheckout-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git checkout</span> has many uses,
+        but the main one is to switch between branches.</br>
+        For example, to switch from master branch to dev branch,
+        I would type <span class="cmd">git checkout dev</span>.
+        After that, if I do a git commit, notice where it goes. Try it.
+      </p>
+      <p>
+        In addition to checking out branches, you can also checkout individual commits. Try it.</br>
+        Make a new commit and then type <span class="cmd">git checkout bb92e0e</span>
+        and see what happens.
+      </p>
+      <p>
+        Type <span class="cmd">git commit</span>, <span class="cmd">git branch</span>,
+        and <span class="cmd">git checkout</span> commands to your hearts desire
+        until you understand this concept.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitCheckout-b-Container" class="twelvecol concept-container">
+      <p>
+        You can combine <span class="cmd">git branch</span> and <span class="cmd">git checkout</span>
+        into a single command by typing <span class="cmd">git checkout -b branchname</span>.
+        This will create the branch if it does not already exist and immediately check it out.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitReset-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git reset</span> will move HEAD and the current branch back to wherever
+        you specify, abandoning any commits that may be left behind. This is useful to undo a commit
+        that you no longer need.
+      </p>
+      <p>
+        This command is normally used with one of three flags: "--soft", "--mixed", and "--hard".
+        The soft and mixed flags deal with what to do with the work that was inside the commit after
+        you reset, and you can read about it <a href="http://git-scm.com/2011/07/11/reset.html">here</a>.
+        Since this visualization cannot graphically display that work, only the "--hard" flag will work
+        on this site.
+      </p>
+      <p>
+        The ref "HEAD^" is usually used together with this command. "HEAD^" means "the commit right
+        before HEAD. "HEAD^^" means "two commits before HEAD", and so on.
+      </p>
+      <p>
+        Note that you must <b>never</b> use <span class="cmd">git reset</span> to abandon commits
+        that have already been pushed and merged into the origin. This can cause your local repository
+        to become out of sync with the origin. Don't do it unless you really know what you're doing.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitRevert-Container" class="twelvecol concept-container">
+      <p>
+        To undo commits that have already been pushed and shared with the team, we cannot use the
+        <span class="cmd">git reset</span> command. Instead, we have to use <span class="cmd">git revert</span>.
+      </p>
+      <p>
+        <span class="cmd">git revert</span> will create a new commit that will undo all of the work that
+        was done in the commit you want to revert.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitMerge-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git merge</span> will create a new commit with two parents. The resulting
+        commit snapshot will have the all of the work that has been done in both branches.
+      </p>
+      <p>
+        If there was no divergence between the two commits, git will do a "fast-forward" method merge.</br>
+        To see this happen, checkout the 'ff' branch and then type <span class="cmd">git merge dev</span>.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitRebase-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git rebase</span> will take the commits on this branch and "move" them so that their
+        new "base" is at the point you specify.
+      </p>
+      <p>
+        You should pay close attention to the commit IDs of the circles as they move when you do this exercise.
+      </p>
+      <p>
+        The reason I put "move" in quotations because this process actually generates brand new commits with
+        completely different IDs than the old commits, and leaves the old commits where they were. For this reason,
+        you never want to rebase commits that have already been shared with the team you are working with.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitFetch-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git fetch</span> will update all of the "remote tracking branches" in your local repository.
+        Remote tracking branches are tagged in grey.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitPull-Container" class="twelvecol concept-container">
+      <p>
+        A <span class="cmd">git pull</span> is a two step process that first does a <span class="cmd">git fetch</span>,
+        and then does a <span class="cmd">git merge</span> of the remote tracking branch associated with your current branch.
+        If you have no current branch, the process will stop after fetching.
+      </p>
+      <p>
+        If the argument "--rebase" was given by typing <span class="cmd">git pull --rebase</span>, the second step of
+        pull process will be a rebase instead of a merge. This can be set to the default behavior by configuration by typing:
+        <span class="cmd">git config branch.BRANCHNAME.rebase true</span>.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitPush-Container" class="twelvecol concept-container">
+      <p>
+        A <span class="cmd">git push</span> will find the commits you have on your local branch that the corresponding branch
+        on the origin server does not have, and send them to the remote repository.
+      </p>
+      <p>
+        By default, all pushes must cause a fast-forward merge on the remote repository. If there is any divergence between
+        your local branch and the remote branch, your push will be rejected. In this scenario, you need to pull first and then
+        you will be able to push again.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitClean-Container" class="twelvecol concept-container">
+      <p>
+        One simple example of the use of <span class="cmd">git reset</span> is to completely restore your local repository
+        state to that of the origin.</br>
+        You can do so by typing <span class="cmd">git reset origin/master</span>.
+      </p>
+      <p>
+        Note that this won't delete untracked files, you will have to delete those separately with
+        the command <span class="cmd">git clean -df</span>.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitFetchRebase-Container" class="twelvecol concept-container">
+      <p>
+        Below is a situation in which you are working in a local branch that is all your own. You want to receive the latest code
+        from the origin server's master branch. To update your local branch, you can do it without having to switch branches!
+      </p>
+      <p>
+        First do a <span class="cmd">git fetch</span>, then type <span class="cmd">git rebase origin/master</span>!
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitDeleteBranches-Container" class="twelvecol concept-container">
+      <p>
+        <span class="cmd">git branch -d</span> is used to delete branches.
+        I have pre-created a bunch of branches for you to delete in the playground below.
+        Have at it.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+    <div id="ExplainGitFree-Container" class="twelvecol concept-container">
+      <p>
+        Do whatever you want in this free playground.
+      </p>
+      <div class="playground-container"></div>
+    </div>
+  </div>
+  <div class="row">
+    <div class="twelvecol">
+      <h2>Specific Examples</h2>
+      <p>Below I have created some specific real-world scenarios that I feel are quite common and useful.</p>
+    </div>
+  </div>
+  <div class="row example-list">
+    <div class="tencol">
+      <a id="open-clean" class="openswitch" href="#clean">Restore Local Branch to State on Origin Server</a>
+      <a id="open-fetchrebase" class="openswitch" href="#fetchrebase">Update Private Local Branch with Latest from Origin</a>
+      <a id="open-deletebranches" class="openswitch" href="#deletebranches">Deleting Local Branches</a>
+    </div>
+    <div class="twocol last">
+      <a id="open-freeplay" class="openswitch" href="#freeplay">Free Playground</a>
+      <a id="open-zen" class="openswitch" href="#zen">Zen Mode</a>
+    </div>
+  </div>
+</div>
+
+<div id="ExplainGitZen-Container" style="display:none">
+    <div class="playground-container"></div>
+</div>
+
+<svg version="1.1" baseProfile="full"
+     xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     xmlns:ev="http://www.w3.org/2001/xml-events"
+     width="0" height="0">
+
+    <marker id="triangle" refX="5" refY="5" markerUnits="strokeWidth" fill="#666"
+            markerWidth="4" markerHeight="3" orient="auto" viewBox="0 0 10 10">
+        <path d="M 0 0 L 10 5 L 0 10 z"/>
+    </marker>
+    <marker id="faded-triangle" refX="5" refY="5" markerUnits="strokeWidth" fill="#DDD"
+            markerWidth="4" markerHeight="3" orient="auto" viewBox="0 0 10 10">
+        <path d="M 0 0 L 10 5 L 0 10 z"/>
+    </marker>
+    <marker id="brown-triangle" refX="5" refY="5" markerUnits="strokeWidth" fill="#663300"
+            markerWidth="4" markerHeight="3" orient="auto" viewBox="0 0 10 10">
+        <path d="M 0 0 L 10 5 L 0 10 z"/>
+    </marker>
+</svg>
+
+<script type="text/javascript">
+require(['explaingit'], function (explainGit) {
+    var examples = {
+        'commit': {
+            name: 'Commit',
+            height: 200,
+            baseLine: 0.4,
+            commitData: [
+                {id: 'e137e9b', tags: ['master']}
+            ],
+            initialMessage: 'Type git commit a few times.'
+        },
+        'branch': {
+            name: 'Branch',
+            baseLine: 0.6,
+            commitData: [
+                {id: 'e137e9b', tags: ['master']}
+            ]
+        },
+        'tag': {
+            name: 'Tag',
+            baseLine: 0.6,
+            commitData: [
+                {id: 'e137e9b', tags: ['master']}
+            ]
+        },
+        'checkout': {
+            name: 'Checkout',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: 'bb92e0e', parent: 'e137e9b', tags: ['master']},
+                {id: 'e088135', parent: 'e137e9b', tags: ['dev']}
+            ],
+            initialMessage:
+                'Use git checkout, git branch, and git commit commands until you understand.'
+        },
+        'checkout-b': {
+            name: 'Checkout-b',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: 'f5b32c8', parent: 'e137e9b'},
+                {id: 'bb92e0e', parent: 'f5b32c8', tags: ['master']},
+                {id: 'e088135', parent: 'e137e9b', tags: ['dev']}
+            ],
+            initialMessage:
+                'Use git checkout -b and git commit commands until you understand.'
+        },
+        'reset': {
+            name: 'Reset',
+            height: 200,
+            baseLine: 0.5,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: '0e70093', parent: 'e137e9b'},
+                {id: '3e33afd', parent: '0e70093', tags: ['master']}
+            ],
+            initialMessage: 'Type "git reset HEAD^".'
+        },
+        'revert': {
+            name: 'Revert',
+            height: 200,
+            baseLine: 0.5,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: '0e70093', parent: 'e137e9b'},
+                {id: '3e33afd', parent: '0e70093', tags: ['master']}
+            ],
+            initialMessage: 'Type "git revert 0e70093".'
+        },
+        'merge': {
+            name: 'Merge',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: 'bb92e0e', parent: 'e137e9b', tags: ['master']},
+                {id: 'f5b32c8', parent: 'e137e9b', tags: ['ff']},
+                {id: 'e088135', parent: 'f5b32c8', tags: ['dev']}
+            ],
+            initialMessage:
+                'Type "git merge dev".'
+        },
+        'rebase': {
+            name: 'Rebase',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: 'bb92e0e', parent: 'e137e9b', tags: ['master']},
+                {id: 'f5b32c8', parent: 'e137e9b'},
+                {id: 'e088135', parent: 'f5b32c8', tags: ['dev']}
+            ],
+            currentBranch: 'dev',
+            initialMessage:
+                'Type "git rebase master".'
+        },
+        'fetch': {
+            name: 'Fetch',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b', tags: ['origin/master']},
+                {id: '6ce726f', parent: 'e137e9b'},
+                {id: 'bb92e0e', parent: '6ce726f', tags: ['master']},
+                {id: '0cff760', parent: 'e137e9b', tags: ['origin/dev']},
+                {id: '4ed301d', parent: '0cff760', tags: ['dev']}
+            ],
+            originData: [
+                {id: 'e137e9b'},
+                {id: '7eb7654', parent: 'e137e9b'},
+                {id: '090e2b8', parent: '7eb7654'},
+                {id: 'ee5df4b', parent: '090e2b8', tags: ['master']},
+                {id: '0cff760', parent: 'e137e9b'},
+                {id: '2f8d946', parent: '0cff760'},
+                {id: '29235ca', parent: '2f8d946', tags: ['dev']}
+            ],
+            initialMessage:
+                'Carefully compare the commit IDs between the origin and the local repository. ' +
+                'Then type "git fetch".'
+        },
+        'pull': {
+            name: 'Pull',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b', tags: ['origin/master']},
+                {id: '46d095b', parent: 'e137e9b', tags: ['master']}
+            ],
+            originData: [
+                {id: 'e137e9b'},
+                {id: '7eb7654', parent: 'e137e9b'},
+                {id: '090e2b8', parent: '7eb7654'},
+                {id: 'ee5df4b', parent: '090e2b8', tags: ['master']}
+            ],
+            initialMessage:
+                'Carefully compare the commit IDs between the origin and the local repository. ' +
+                'Then type "git pull".'
+        },
+        'push': {
+            name: 'Push',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b', tags: ['origin/master']},
+                {id: '46d095b', parent: 'e137e9b', tags: ['master']}
+            ],
+            originData: [
+                {id: 'e137e9b'},
+                {id: '7eb7654', parent: 'e137e9b', tags: ['master']}
+            ],
+            initialMessage:
+                'Carefully compare the commit IDs between the origin and the local repository. ' +
+                'Then type "git push".'
+        },
+        'clean': {
+            name: 'Clean',
+            height: 200,
+            baseLine: 0.4,
+            commitData: [
+                {id: 'e137e9b', tags: ['origin/master']},
+                {id: '0e70093', parent: 'e137e9b'},
+                {id: '3e33afd', parent: '0e70093', tags: ['master']}
+            ],
+            initialMessage: 'Type "git reset origin/master".'
+        },
+        'fetchrebase': {
+            name: 'FetchRebase',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b', tags: ['origin/master', 'master']},
+                {id: '46d095b', parent: 'e137e9b'},
+                {id: 'dccdc4d', parent: '46d095b', tags: ['my-branch']}
+            ],
+            currentBranch: 'my-branch',
+            originData: [
+                {id: 'e137e9b'},
+                {id: '7eb7654', parent: 'e137e9b'},
+                {id: '090e2b8', parent: '7eb7654'},
+                {id: 'ee5df4b', parent: '090e2b8', tags: ['master']}
+            ],
+            initialMessage:
+                'First type "git fetch". Then type "git rebase origin/master".'
+        },
+        'deletebranches': {
+            name: 'DeleteBranches',
+            height: 500,
+            baseLine: 0.6,
+            commitData: [
+                {id: 'e137e9b'},
+                {id: 'bb92e0e', parent: 'e137e9b'},
+                {id: 'd25ee9b', parent: 'bb92e0e', tags: ['master']},
+                {id: '071ff28', parent: 'e137e9b', tags: ['protoss']},
+                {id: 'f5b32c8', parent: 'bb92e0e'},
+                {id: 'e088135', parent: 'f5b32c8', tags: ['zerg']},
+                {id: '9e6c322', parent: 'bb92e0e'},
+                {id: '593ae02', parent: '9e6c322', tags: ['terran']}
+            ],
+            currentBranch: 'terran',
+            initialMessage:
+                'Delete some branches.'
+        },
+        'freeplay': {
+            name: 'Free',
+            height: 500,
+            commitData: [
+                {id: 'e137e9b', tags: ['origin/master', 'master']}
+            ],
+            originData: [
+                {id: 'e137e9b'},
+                {id: '7eb7654', parent: 'e137e9b'},
+                {id: '090e2b8', parent: '7eb7654'},
+                {id: 'ee5df4b', parent: '090e2b8', tags: ['master']}
+            ],
+            initialMessage:
+                'Have fun.'
+        }
+    };
+
+    window.addEventListener('hashchange', open, false);
+    window.addEventListener('load', open, false);
+    
+    function open() {
+        var hash = window.location.hash.substr(1),
+            linkId = 'open-' + hash,
+            example = examples[hash];
+        
+        if (example) {
+            explainGit.reset();
+            document.getElementById(linkId).classList.add('selected');
+            explainGit.open(example);
+        } else if (hash === 'zen') {
+            var elements = document.getElementsByClassName('row');
+            for(var i = 0; i != elements.length; ++i)
+            {
+              elements[i].style.display = 'none';
+            }
+            document.getElementById('fork-me').style.display = 'none';
+            
+            explainGit.reset();
+
+            explainGit.open({
+                name: 'Zen',
+                height: '100%',
+                commitData: [
+                    {id: 'e137e9b', tags: ['master'], message: 'first commit'}
+                ],
+                initialMessage:
+                    'Have fun.'
+            });
+        }
+    }
+});
+</script>
+</body>
+</html>
diff --git a/explain-git-with-d3/js/controlbox.js b/explain-git-with-d3/js/controlbox.js
new file mode 100644 (file)
index 0000000..b0e664d
--- /dev/null
@@ -0,0 +1,520 @@
+define(['d3'], function () {
+    "use strict";
+
+    /**
+     * @class ControlBox
+     * @constructor
+     */
+    function ControlBox(config) {
+        this.historyView = config.historyView;
+        this.originView = config.originView;
+        this.initialMessage = config.initialMessage || 'Enter git commands below.';
+        this._commandHistory = [];
+        this._currentCommand = -1;
+        this._tempCommand = '';
+        this.rebaseConfig = {}; // to configure branches for rebase
+    }
+
+    ControlBox.prototype = {
+        render: function (container) {
+            var cBox = this,
+                cBoxContainer, log, input;
+
+            cBoxContainer = container.append('div')
+                .classed('control-box', true);
+
+
+            log = cBoxContainer.append('div')
+                .classed('log', true);
+
+            input = cBoxContainer.append('input')
+                .attr('type', 'text')
+                .attr('placeholder', 'enter git command');
+
+            input.on('keyup', function () {
+                var e = d3.event;
+
+                switch (e.keyCode) {
+                case 13:
+                    if (this.value.trim() === '') {
+                        break;
+                    }
+
+                    cBox._commandHistory.unshift(this.value);
+                    cBox._tempCommand = '';
+                    cBox._currentCommand = -1;
+                    cBox.command(this.value);
+                    this.value = '';
+                    e.stopImmediatePropagation();
+                    break;
+                case 38:
+                    var previousCommand = cBox._commandHistory[cBox._currentCommand + 1];
+                    if (cBox._currentCommand === -1) {
+                        cBox._tempCommand = this.value;
+                    }
+
+                    if (typeof previousCommand === 'string') {
+                        cBox._currentCommand += 1;
+                        this.value = previousCommand;
+                        this.value = this.value; // set cursor to end
+                    }
+                    e.stopImmediatePropagation();
+                    break;
+                case 40:
+                    var nextCommand = cBox._commandHistory[cBox._currentCommand - 1];
+                    if (typeof nextCommand === 'string') {
+                        cBox._currentCommand -= 1;
+                        this.value = nextCommand;
+                        this.value = this.value; // set cursor to end
+                    } else {
+                        cBox._currentCommand = -1;
+                        this.value = cBox._tempCommand;
+                        this.value = this.value; // set cursor to end
+                    }
+                    e.stopImmediatePropagation();
+                    break;
+                }
+            });
+
+            this.container = cBoxContainer;
+            this.log = log;
+            this.input = input;
+
+            this.info(this.initialMessage);
+        },
+
+        destroy: function () {
+            this.log.remove();
+            this.input.remove();
+            this.container.remove();
+
+            for (var prop in this) {
+                if (this.hasOwnProperty(prop)) {
+                    this[prop] = null;
+                }
+            }
+        },
+
+        _scrollToBottom: function () {
+            var log = this.log.node();
+            log.scrollTop = log.scrollHeight;
+        },
+
+        command: function (entry) {
+            if (entry.trim === '') {
+                return;
+            }
+
+            var split = entry.split(' ');
+
+            this.log.append('div')
+                .classed('command-entry', true)
+                .html(entry);
+
+            this._scrollToBottom();
+
+            if (split[0] !== 'git') {
+                return this.error();
+            }
+
+            var method = split[1],
+                args = split.slice(2);
+
+            try {
+                if (typeof this[method] === 'function') {
+                    this[method](args);
+                } else {
+                    this.error();
+                }
+            } catch (ex) {
+                var msg = (ex && ex.message) ? ex.message: null;
+                this.error(msg);
+            }
+        },
+
+        info: function (msg) {
+            this.log.append('div').classed('info', true).html(msg);
+            this._scrollToBottom();
+        },
+
+        error: function (msg) {
+            msg = msg || 'I don\'t understand that.';
+            this.log.append('div').classed('error', true).html(msg);
+            this._scrollToBottom();
+        },
+
+        commit: function (args) {
+            if (args.length >= 2) {
+                var arg = args.shift();
+
+                switch (arg) {
+                    case '-m':
+                        var message = args.join(" ");
+                        this.historyView.commit({},message);
+                        break;
+                    default:
+                        this.historyView.commit();
+                        break;
+                }
+            } else {
+                this.historyView.commit();
+            }
+        },
+
+        branch: function (args) {
+            if (args.length < 1) {
+                this.info(
+                    'You need to give a branch name. ' +
+                    'Normally if you don\'t give a name, ' +
+                    'this command will list your local branches on the screen.'
+                );
+
+                return;
+            }
+
+            while (args.length > 0) {
+                var arg = args.shift();
+
+                switch (arg) {
+                case '--remote':
+                case '-r':
+                    this.info(
+                        'This command normally displays all of your remote tracking branches.'
+                    );
+                    args.length = 0;
+                    break;
+                case '--all':
+                case '-a':
+                    this.info(
+                        'This command normally displays all of your tracking branches, both remote and local.'
+                    );
+                    break;
+                case '--delete':
+                case '-d':
+                    var name = args.pop();
+                    this.historyView.deleteBranch(name);
+                    break;
+                default:
+                    if (arg.charAt(0) === '-') {
+                        this.error();
+                    } else {
+                        var remainingArgs = [arg].concat(args);
+                        args.length = 0;
+                        this.historyView.branch(remainingArgs.join(' '));
+                    }
+                }
+            }
+        },
+
+        checkout: function (args) {
+            while (args.length > 0) {
+                var arg = args.shift();
+
+                switch (arg) {
+                case '-b':
+                    var name = args[args.length - 1];
+                    try {
+                        this.historyView.branch(name);
+                    } catch (err) {
+                        if (err.message.indexOf('already exists') === -1) {
+                            throw new Error(err.message);
+                        }
+                    }
+                    break;
+                default:
+                    var remainingArgs = [arg].concat(args);
+                    args.length = 0;
+                    this.historyView.checkout(remainingArgs.join(' '));
+                }
+            }
+        },
+
+        tag: function (args) {
+            if (args.length < 1) {
+                this.info(
+                    'You need to give a tag name. ' +
+                    'Normally if you don\'t give a name, ' +
+                    'this command will list your local tags on the screen.'
+                );
+
+                return;
+            }
+            
+            while (args.length > 0) {
+                var arg = args.shift();
+
+                try {
+                    this.historyView.tag(arg);
+                } catch (err) {
+                    if (err.message.indexOf('already exists') === -1) {
+                        throw new Error(err.message);
+                    }
+                }
+            }
+        },
+
+        reset: function (args) {
+            while (args.length > 0) {
+                var arg = args.shift();
+
+                switch (arg) {
+                case '--soft':
+                    this.info(
+                        'The "--soft" flag works in real git, but ' +
+                        'I am unable to show you how it works in this demo. ' +
+                        'So I am just going to show you what "--hard" looks like instead.'
+                    );
+                    break;
+                case '--mixed':
+                    this.info(
+                        'The "--mixed" flag works in real git, but ' +
+                        'I am unable to show you how it works in this demo.'
+                    );
+                    break;
+                case '--hard':
+                    this.historyView.reset(args.join(' '));
+                    args.length = 0;
+                    break;
+                default:
+                    var remainingArgs = [arg].concat(args);
+                    args.length = 0;
+                    this.info('Assuming "--hard".');
+                    this.historyView.reset(remainingArgs.join(' '));
+                }
+            }
+        },
+
+        clean: function (args) {
+            this.info('Deleting all of your untracked files...');
+        },
+
+        revert: function (args) {
+            this.historyView.revert(args.shift());
+        },
+
+        merge: function (args) {
+            var noFF = false;
+            var branch = args[0];
+            if (args.length === 2)
+            {
+                if (args[0] === '--no-ff') {
+                    noFF = true;
+                    branch = args[1];
+                } else if (args[1] === '--no-ff') {
+                    noFF = true;
+                    branch = args[0];
+                } else {
+                    this.info('This demo only supports the --no-ff switch..');
+                }
+            }
+            var result = this.historyView.merge(branch, noFF);
+
+            if (result === 'Fast-Forward') {
+                this.info('You have performed a fast-forward merge.');
+            }
+        },
+
+        rebase: function (args) {
+            var ref = args.shift(),
+                result = this.historyView.rebase(ref);
+
+            if (result === 'Fast-Forward') {
+                this.info('Fast-forwarded to ' + ref + '.');
+            }
+        },
+
+        fetch: function () {
+            if (!this.originView) {
+                throw new Error('There is no remote server to fetch from.');
+            }
+
+            var origin = this.originView,
+                local = this.historyView,
+                remotePattern = /^origin\/([^\/]+)$/,
+                rtb, isRTB, fb,
+                fetchBranches = {},
+                fetchIds = [], // just to make sure we don't fetch the same commit twice
+                fetchCommits = [], fetchCommit,
+                resultMessage = '';
+
+            // determine which branches to fetch
+            for (rtb = 0; rtb < local.branches.length; rtb++) {
+                isRTB = remotePattern.exec(local.branches[rtb]);
+                if (isRTB) {
+                    fetchBranches[isRTB[1]] = 0;
+                }
+            }
+
+            // determine which commits the local repo is missing from the origin
+            for (fb in fetchBranches) {
+                if (origin.branches.indexOf(fb) > -1) {
+                    fetchCommit = origin.getCommit(fb);
+
+                    var notInLocal = local.getCommit(fetchCommit.id) === null;
+                    while (notInLocal) {
+                        if (fetchIds.indexOf(fetchCommit.id) === -1) {
+                            fetchCommits.unshift(fetchCommit);
+                            fetchIds.unshift(fetchCommit.id);
+                        }
+                        fetchBranches[fb] += 1;
+                        fetchCommit = origin.getCommit(fetchCommit.parent);
+                        notInLocal = local.getCommit(fetchCommit.id) === null;
+                    }
+                }
+            }
+
+            // add the fetched commits to the local commit data
+            for (var fc = 0; fc < fetchCommits.length; fc++) {
+                fetchCommit = fetchCommits[fc];
+                local.commitData.push({
+                    id: fetchCommit.id,
+                    parent: fetchCommit.parent,
+                    tags: []
+                });
+            }
+
+            // update the remote tracking branch tag locations
+            for (fb in fetchBranches) {
+                if (origin.branches.indexOf(fb) > -1) {
+                    var remoteLoc = origin.getCommit(fb).id;
+                    local.moveTag('origin/' + fb, remoteLoc);
+                }
+
+                resultMessage += 'Fetched ' + fetchBranches[fb] + ' commits on ' + fb + '.</br>';
+            }
+
+            this.info(resultMessage);
+
+            local.renderCommits();
+        },
+
+        pull: function (args) {
+            var control = this,
+                local = this.historyView,
+                currentBranch = local.currentBranch,
+                rtBranch = 'origin/' + currentBranch,
+                isFastForward = false;
+
+            this.fetch();
+
+            if (!currentBranch) {
+                throw new Error('You are not currently on a branch.');
+            }
+
+            if (local.branches.indexOf(rtBranch) === -1) {
+                throw new Error('Current branch is not set up for pulling.');
+            }
+
+            setTimeout(function () {
+                try {
+                    if (args[0] === '--rebase' || control.rebaseConfig[currentBranch] === 'true') {
+                        isFastForward = local.rebase(rtBranch) === 'Fast-Forward';
+                    } else {
+                        isFastForward = local.merge(rtBranch) === 'Fast-Forward';
+                    }
+                } catch (error) {
+                    control.error(error.message);
+                }
+
+                if (isFastForward) {
+                    control.info('Fast-forwarded to ' + rtBranch + '.');
+                }
+            }, 750);
+        },
+
+        push: function (args) {
+            var control = this,
+                local = this.historyView,
+                remoteName = args.shift() || 'origin',
+                remote = this[remoteName + 'View'],
+                branchArgs = args.pop(),
+                localRef = local.currentBranch,
+                remoteRef = local.currentBranch,
+                localCommit, remoteCommit,
+                findCommitsToPush,
+                isCommonCommit,
+                toPush = [];
+
+            if (remoteName === 'history') {
+                throw new Error('Sorry, you can\'t have a remote named "history" in this example.');
+            }
+
+            if (!remote) {
+                throw new Error('There is no remote server named "' + remoteName + '".');
+            }
+
+            if (branchArgs) {
+                branchArgs = /^([^:]*)(:?)(.*)$/.exec(branchArgs);
+
+                branchArgs[1] && (localRef = branchArgs[1]);
+                branchArgs[2] === ':' && (remoteRef = branchArgs[3]);
+            }
+
+            if (local.branches.indexOf(localRef) === -1) {
+                throw new Error('Local ref: ' + localRef + ' does not exist.');
+            }
+
+            if (!remoteRef) {
+                throw new Error('No remote branch was specified to push to.');
+            }
+
+            localCommit = local.getCommit(localRef);
+            remoteCommit = remote.getCommit(remoteRef);
+
+            findCommitsToPush = function findCommitsToPush(localCommit) {
+                var commitToPush,
+                    isCommonCommit = remote.getCommit(localCommit.id) !== null;
+
+                while (!isCommonCommit) {
+                    commitToPush = {
+                        id: localCommit.id,
+                        parent: localCommit.parent,
+                        tags: []
+                    };
+
+                    if (typeof localCommit.parent2 === 'string') {
+                        commitToPush.parent2 = localCommit.parent2;
+                        findCommitsToPush(local.getCommit(localCommit.parent2));
+                    }
+
+                    toPush.unshift(commitToPush);
+                    localCommit = local.getCommit(localCommit.parent);
+                    isCommonCommit = remote.getCommit(localCommit.id) !== null;
+                }
+            };
+
+            // push to an existing branch on the remote
+            if (remoteCommit && remote.branches.indexOf(remoteRef) > -1) {
+                if (!local.isAncestor(remoteCommit.id, localCommit.id)) {
+                    throw new Error('Push rejected. Non fast-forward.');
+                }
+
+                isCommonCommit = localCommit.id === remoteCommit.id;
+
+                if (isCommonCommit) {
+                    return this.info('Everything up-to-date.');
+                }
+
+                findCommitsToPush(localCommit);
+
+                remote.commitData = remote.commitData.concat(toPush);
+                remote.moveTag(remoteRef, toPush[toPush.length - 1].id);
+                remote.renderCommits();
+            } else {
+                this.info('Sorry, creating new remote branches is not supported yet.');
+            }
+        },
+
+        config: function (args) {
+            var path = args.shift().split('.');
+
+            if (path[0] === 'branch') {
+                if (path[2] === 'rebase') {
+                    this.rebase[path[1]] = args.pop();
+                }
+            }
+        }
+    };
+
+    return ControlBox;
+});
diff --git a/explain-git-with-d3/js/explaingit.js b/explain-git-with-d3/js/explaingit.js
new file mode 100644 (file)
index 0000000..9501ee3
--- /dev/null
@@ -0,0 +1,74 @@
+define(['historyview', 'controlbox', 'd3'], function (HistoryView, ControlBox, d3) {
+    var prefix = 'ExplainGit',
+        openSandBoxes = [],
+        open,
+        reset,
+        explainGit;
+
+    open = function (_args) {
+        var args = Object.create(_args),
+            name = prefix + args.name,
+            containerId = name + '-Container',
+            container = d3.select('#' + containerId),
+            playground = container.select('.playground-container'),
+            historyView, originView = null,
+            controlBox;
+
+        container.style('display', 'block');
+
+        args.name = name;
+        historyView = new HistoryView(args);
+
+        if (args.originData) {
+            originView = new HistoryView({
+                name: name + '-Origin',
+                width: 300,
+                height: 225,
+                commitRadius: 15,
+                remoteName: 'origin',
+                commitData: args.originData
+            });
+
+            originView.render(playground);
+        }
+
+        controlBox = new ControlBox({
+            historyView: historyView,
+            originView: originView,
+            initialMessage: args.initialMessage
+        });
+
+        controlBox.render(playground);
+        historyView.render(playground);
+
+        openSandBoxes.push({
+            hv: historyView,
+            cb: controlBox,
+            container: container
+        });
+    };
+
+    reset = function () {
+        for (var i = 0; i < openSandBoxes.length; i++) {
+            var osb = openSandBoxes[i];
+            osb.hv.destroy();
+            osb.cb.destroy();
+            osb.container.style('display', 'none');
+        }
+
+        openSandBoxes.length = 0;
+        d3.selectAll('a.openswitch').classed('selected', false);
+    };
+
+    explainGit = {
+        HistoryView: HistoryView,
+        ControlBox: ControlBox,
+        generateId: HistoryView.generateId,
+        open: open,
+        reset: reset
+    };
+
+    window.explainGit = explainGit;
+
+    return explainGit;
+});
\ No newline at end of file
diff --git a/explain-git-with-d3/js/historyview.js b/explain-git-with-d3/js/historyview.js
new file mode 100644 (file)
index 0000000..683201e
--- /dev/null
@@ -0,0 +1,1061 @@
+define(['d3'], function () {
+    "use strict";
+
+    var REG_MARKER_END = 'url(#triangle)',
+        MERGE_MARKER_END = 'url(#brown-triangle)',
+        FADED_MARKER_END = 'url(#faded-triangle)',
+
+        preventOverlap,
+        applyBranchlessClass,
+        cx, cy, fixCirclePosition,
+        px1, py1, fixPointerStartPosition,
+        px2, py2, fixPointerEndPosition,
+        fixIdPosition, tagY;
+
+    preventOverlap = function preventOverlap(commit, view) {
+        var commitData = view.commitData,
+            baseLine = view.baseLine,
+            shift = view.commitRadius * 4.5,
+            overlapped = null;
+
+        for (var i = 0; i < commitData.length; i++) {
+            var c = commitData[i];
+            if (c.cx === commit.cx && c.cy === commit.cy && c !== commit) {
+                overlapped = c;
+                break;
+            }
+        }
+
+        if (overlapped) {
+            var oParent = view.getCommit(overlapped.parent),
+                parent = view.getCommit(commit.parent);
+
+            if (overlapped.cy < baseLine) {
+                overlapped = oParent.cy < parent.cy ? overlapped : commit;
+                overlapped.cy -= shift;
+            } else {
+                overlapped = oParent.cy > parent.cy ? overlapped : commit;
+                overlapped.cy += shift;
+            }
+
+            preventOverlap(overlapped, view);
+        }
+    };
+
+    applyBranchlessClass = function (selection) {
+        if (selection.empty()) {
+            return;
+        }
+
+        selection.classed('branchless', function (d) {
+            return d.branchless;
+        });
+
+        if (selection.classed('commit-pointer')) {
+            selection.attr('marker-end', function (d) {
+                return d.branchless ? FADED_MARKER_END : REG_MARKER_END;
+            });
+        } else if (selection.classed('merge-pointer')) {
+            selection.attr('marker-end', function (d) {
+                return d.branchless ? FADED_MARKER_END : MERGE_MARKER_END;
+            });
+        }
+    };
+
+    cx = function (commit, view) {
+        var parent = view.getCommit(commit.parent),
+            parentCX = parent.cx;
+
+        if (typeof commit.parent2 === 'string') {
+            var parent2 = view.getCommit(commit.parent2);
+
+            parentCX = parent.cx > parent2.cx ? parent.cx : parent2.cx;
+        }
+
+        return parentCX + (view.commitRadius * 4.5);
+    };
+
+    cy = function (commit, view) {
+        var parent = view.getCommit(commit.parent),
+            parentCY = parent.cy || cy(parent, view),
+            baseLine = view.baseLine,
+            shift = view.commitRadius * 4.5,
+            branches = [], // count the existing branches
+            branchIndex = 0;
+
+        for (var i = 0; i < view.commitData.length; i++) {
+            var d = view.commitData[i];
+
+            if (d.parent === commit.parent) {
+                branches.push(d.id);
+            }
+        }
+
+        branchIndex = branches.indexOf(commit.id);
+
+        if (commit.isNoFFBranch === true) {
+            branchIndex++;
+        }
+        if (commit.isNoFFCommit === true) {
+            branchIndex--;
+        }
+
+        if (parentCY === baseLine) {
+            var direction = 1;
+            for (var bi = 0; bi < branchIndex; bi++) {
+                direction *= -1;
+            }
+
+            shift *= Math.ceil(branchIndex / 2);
+
+            return parentCY + (shift * direction);
+        }
+
+        if (parentCY < baseLine) {
+            return parentCY - (shift * branchIndex);
+        } else if (parentCY > baseLine) {
+            return parentCY + (shift * branchIndex);
+        }
+    };
+
+    fixCirclePosition = function (selection) {
+        selection
+            .attr('cx', function (d) {
+                return d.cx;
+            })
+            .attr('cy', function (d) {
+                return d.cy;
+            });
+    };
+
+    // calculates the x1 point for commit pointer lines
+    px1 = function (commit, view, pp) {
+        pp = pp || 'parent';
+
+        var parent = view.getCommit(commit[pp]),
+            startCX = commit.cx,
+            diffX = startCX - parent.cx,
+            diffY = parent.cy - commit.cy,
+            length = Math.sqrt((diffX * diffX) + (diffY * diffY));
+
+        return startCX - (view.pointerMargin * (diffX / length));
+    };
+
+    // calculates the y1 point for commit pointer lines
+    py1 = function (commit, view, pp) {
+        pp = pp || 'parent';
+
+        var parent = view.getCommit(commit[pp]),
+            startCY = commit.cy,
+            diffX = commit.cx - parent.cx,
+            diffY = parent.cy - startCY,
+            length = Math.sqrt((diffX * diffX) + (diffY * diffY));
+
+        return startCY + (view.pointerMargin * (diffY / length));
+    };
+
+    fixPointerStartPosition = function (selection, view) {
+        selection.attr('x1', function (d) {
+            return px1(d, view);
+        }).attr('y1', function (d) {
+            return py1(d, view);
+        });
+    };
+
+    px2 = function (commit, view, pp) {
+        pp = pp || 'parent';
+
+        var parent = view.getCommit(commit[pp]),
+            endCX = parent.cx,
+            diffX = commit.cx - endCX,
+            diffY = parent.cy - commit.cy,
+            length = Math.sqrt((diffX * diffX) + (diffY * diffY));
+
+        return endCX + (view.pointerMargin * 1.2 * (diffX / length));
+    };
+
+    py2 = function (commit, view, pp) {
+        pp = pp || 'parent';
+
+        var parent = view.getCommit(commit[pp]),
+            endCY = parent.cy,
+            diffX = commit.cx - parent.cx,
+            diffY = endCY - commit.cy,
+            length = Math.sqrt((diffX * diffX) + (diffY * diffY));
+
+        return endCY - (view.pointerMargin * 1.2 * (diffY / length));
+    };
+
+    fixPointerEndPosition = function (selection, view) {
+        selection.attr('x2', function (d) {
+            return px2(d, view);
+        }).attr('y2', function (d) {
+            return py2(d, view);
+        });
+    };
+
+    fixIdPosition = function (selection, view, delta) {
+        selection.attr('x', function (d) {
+            return d.cx;
+        }).attr('y', function (d) {
+            return d.cy + view.commitRadius + delta;
+        });
+    };
+
+    tagY = function tagY(t, view) {
+        var commit = view.getCommit(t.commit),
+            commitCY = commit.cy,
+            tags = commit.tags,
+            tagIndex = tags.indexOf(t.name);
+
+        if (tagIndex === -1) {
+            tagIndex = tags.length;
+        }
+
+        if (commitCY < (view.baseLine)) {
+            return commitCY - 45 - (tagIndex * 25);
+        } else {
+            return commitCY + 50 + (tagIndex * 25);
+        }
+    };
+
+    /**
+     * @class HistoryView
+     * @constructor
+     */
+    function HistoryView(config) {
+        var commitData = config.commitData || [],
+            commit;
+
+        for (var i = 0; i < commitData.length; i++) {
+            commit = commitData[i];
+            !commit.parent && (commit.parent = 'initial');
+            !commit.tags && (commit.tags = []);
+        }
+
+        this.name = config.name || 'UnnamedHistoryView';
+        this.commitData = commitData;
+
+        this.branches = [];
+        this.currentBranch = config.currentBranch || 'master';
+
+        this.width = config.width;
+        this.height = config.height || 400;
+        this.orginalBaseLine = config.baseLine;
+        this.baseLine = this.height * (config.baseLine || 0.6);
+
+        this.commitRadius = config.commitRadius || 20;
+        this.pointerMargin = this.commitRadius * 1.3;
+
+        this.isRemote = typeof config.remoteName === 'string';
+        this.remoteName = config.remoteName;
+
+        this.initialCommit = {
+            id: 'initial',
+            parent: null,
+            cx: -(this.commitRadius * 2),
+            cy: this.baseLine
+        };
+    }
+
+    HistoryView.generateId = function () {
+        return Math.floor((1 + Math.random()) * 0x10000000).toString(16).substring(1);
+    };
+
+    HistoryView.prototype = {
+        /**
+         * @method getCommit
+         * @param ref {String} the id or a tag name that refers to the commit
+         * @return {Object} the commit datum object
+         */
+        getCommit: function getCommit(ref) {
+            var commitData = this.commitData,
+                headMatcher = /HEAD(\^+)/.exec(ref),
+                matchedCommit = null;
+
+            if (ref === 'initial') {
+                return this.initialCommit;
+            }
+
+            if (headMatcher) {
+                ref = 'HEAD';
+            }
+
+            for (var i = 0; i < commitData.length; i++) {
+                var commit = commitData[i];
+                if (commit === ref) {
+                    matchedCommit = commit;
+                    break;
+                }
+
+                if (commit.id === ref) {
+                    matchedCommit = commit;
+                    break;
+                }
+
+                var matchedTag = function() { 
+                    for (var j = 0; j < commit.tags.length; j++) {
+                        var tag = commit.tags[j];
+                        if (tag === ref) {
+                            matchedCommit = commit;
+                            return true;
+                        }
+                        
+                        if (tag.indexOf('[') === 0 && tag.indexOf(']') === tag.length - 1) {
+                            tag = tag.substring(1, tag.length - 1);
+                        }
+                        if (tag === ref) {
+                            matchedCommit = commit;
+                            return true;
+                        }
+                    }
+                }();
+                if (matchedTag === true) {
+                    break;
+                }
+            }
+
+            if (headMatcher && matchedCommit) {
+                for (var h = 0; h < headMatcher[1].length; h++) {
+                    matchedCommit = getCommit.call(this, matchedCommit.parent);
+                }
+            }
+
+            return matchedCommit;
+        },
+
+        /**
+         * @method getCircle
+         * @param ref {String} the id or a tag name that refers to the commit
+         * @return {d3 Selection} the d3 selected SVG circle
+         */
+        getCircle: function (ref) {
+            var circle = this.svg.select('#' + this.name + '-' + ref),
+                commit;
+
+            if (circle && !circle.empty()) {
+                return circle;
+            }
+
+            commit = this.getCommit(ref);
+
+            if (!commit) {
+                return null;
+            }
+
+            return this.svg.select('#' + this.name + '-' + commit.id);
+        },
+
+        getCircles: function () {
+            return this.svg.selectAll('circle.commit');
+        },
+
+        /**
+         * @method render
+         * @param container {String} selector for the container to render the SVG into
+         */
+        render: function (container) {
+            var svgContainer, svg;
+
+            svgContainer = container.append('div')
+                .classed('svg-container', true)
+                .classed('remote-container', this.isRemote);
+                
+            svg = svgContainer.append('svg:svg');
+
+            svg.attr('id', this.name)
+                .attr('width', this.width)
+                .attr('height', this.height);
+
+            if (this.isRemote) {
+                svg.append('svg:text')
+                    .classed('remote-name-display', true)
+                    .text(this.remoteName)
+                    .attr('x', 10)
+                    .attr('y', 25);
+            } else {
+                svg.append('svg:text')
+                    .classed('remote-name-display', true)
+                    .text('Local Repository')
+                    .attr('x', 10)
+                    .attr('y', 25);
+
+                svg.append('svg:text')
+                    .classed('current-branch-display', true)
+                    .attr('x', 10)
+                    .attr('y', 45);
+            }
+
+            this.svgContainer = svgContainer;
+            this.svg = svg;
+            this.arrowBox = svg.append('svg:g').classed('pointers', true);
+            this.commitBox = svg.append('svg:g').classed('commits', true);
+            this.tagBox = svg.append('svg:g').classed('tags', true);
+
+            this.renderCommits();
+
+            this._setCurrentBranch(this.currentBranch);
+        },
+
+        destroy: function () {
+            this.svg.remove();
+            this.svgContainer.remove();
+            clearInterval(this.refreshSizeTimer);
+
+            for (var prop in this) {
+                if (this.hasOwnProperty(prop)) {
+                    this[prop] = null;
+                }
+            }
+        },
+
+        _calculatePositionData: function () {
+            for (var i = 0; i < this.commitData.length; i++) {
+                var commit = this.commitData[i];
+                commit.cx = cx(commit, this);
+                commit.cy = cy(commit, this);
+                preventOverlap(commit, this);
+            }
+        },
+        
+        _resizeSvg: function() {
+            var ele = document.getElementById(this.svg.node().id);
+            var container = ele.parentNode;
+            var currentWidth = ele.offsetWidth;
+            var newWidth;
+
+            if (ele.getBBox().width > container.offsetWidth)
+                newWidth = Math.round(ele.getBBox().width);
+            else
+                newWidth = container.offsetWidth - 5;
+
+            if (currentWidth != newWidth) {
+                this.svg.attr('width', newWidth);
+                container.scrollLeft = container.scrollWidth;
+            }
+        },
+
+        renderCommits: function () {
+            if (typeof this.height === 'string' && this.height.indexOf('%') >= 0) {
+                var perc = this.height.substring(0, this.height.length - 1) / 100.0;
+                var baseLineCalcHeight = Math.round(this.svg.node().parentNode.offsetHeight * perc) - 65;
+                var newBaseLine = Math.round(baseLineCalcHeight * (this.originalBaseLine || 0.6));
+                if (newBaseLine !== this.baseLine) {
+                    this.baseLine = newBaseLine;
+                    this.initialCommit.cy = newBaseLine;
+                    this.svg.attr('height', baseLineCalcHeight);
+                }
+            }
+            this._calculatePositionData();
+            this._calculatePositionData(); // do this twice to make sure
+            this._renderCircles();
+            this._renderPointers();
+            this._renderMergePointers();
+            this._renderIdLabels();
+            this._resizeSvg();
+            this.checkout(this.currentBranch);
+        },
+
+        _renderCircles: function () {
+            var view = this,
+                existingCircles,
+                newCircles;
+
+            existingCircles = this.commitBox.selectAll('circle.commit')
+                .data(this.commitData, function (d) { return d.id; })
+                .attr('id', function (d) {
+                    return view.name + '-' + d.id;
+                })
+                .classed('reverted', function (d) {
+                    return d.reverted;
+                })
+                .classed('rebased', function (d) {
+                    return d.rebased;
+                });
+
+            existingCircles.transition()
+                .duration(500)
+                .call(fixCirclePosition);
+
+            newCircles = existingCircles.enter()
+                .append('svg:circle')
+                .attr('id', function (d) {
+                    return view.name + '-' + d.id;
+                })
+                .classed('commit', true)
+                .classed('merge-commit', function (d) {
+                    return typeof d.parent2 === 'string';
+                })
+                .call(fixCirclePosition)
+                .attr('r', 1)
+                .transition("inflate")
+                .duration(500)
+                .attr('r', this.commitRadius);
+
+        },
+
+        _renderPointers: function () {
+            var view = this,
+                existingPointers,
+                newPointers;
+
+            existingPointers = this.arrowBox.selectAll('line.commit-pointer')
+                .data(this.commitData, function (d) { return d.id; })
+                .attr('id', function (d) {
+                    return view.name + '-' + d.id + '-to-' + d.parent;
+                });
+
+            existingPointers.transition()
+                .duration(500)
+                .call(fixPointerStartPosition, view)
+                .call(fixPointerEndPosition, view);
+
+            newPointers = existingPointers.enter()
+                .append('svg:line')
+                .attr('id', function (d) {
+                    return view.name + '-' + d.id + '-to-' + d.parent;
+                })
+                .classed('commit-pointer', true)
+                .call(fixPointerStartPosition, view)
+                .attr('x2', function () { return d3.select(this).attr('x1'); })
+                .attr('y2', function () {  return d3.select(this).attr('y1'); })
+                .attr('marker-end', REG_MARKER_END)
+                .transition()
+                .duration(500)
+                .call(fixPointerEndPosition, view);
+        },
+
+        _renderMergePointers: function () {
+            var view = this,
+                mergeCommits = [],
+                existingPointers, newPointers;
+
+            for (var i = 0; i < this.commitData.length; i++) {
+                var commit = this.commitData[i];
+                if (typeof commit.parent2 === 'string') {
+                    mergeCommits.push(commit);
+                }
+            }
+
+            existingPointers = this.arrowBox.selectAll('polyline.merge-pointer')
+                .data(mergeCommits, function (d) { return d.id; })
+                .attr('id', function (d) {
+                    return view.name + '-' + d.id + '-to-' + d.parent2;
+                });
+
+            existingPointers.transition().duration(500)
+                .attr('points', function (d) {
+                    var p1 = px1(d, view, 'parent2') + ',' + py1(d, view, 'parent2'),
+                        p2 = px2(d, view, 'parent2') + ',' + py2(d, view, 'parent2');
+
+                    return [p1, p2].join(' ');
+                });
+
+            newPointers = existingPointers.enter()
+                .append('svg:polyline')
+                .attr('id', function (d) {
+                    return view.name + '-' + d.id + '-to-' + d.parent2;
+                })
+                .classed('merge-pointer', true)
+                .attr('points', function (d) {
+                    var x1 = px1(d, view, 'parent2'),
+                        y1 = py1(d, view, 'parent2'),
+                        p1 = x1 + ',' + y1;
+
+                    return [p1, p1].join(' ');
+                })
+                .attr('marker-end', MERGE_MARKER_END)
+                .transition()
+                .duration(500)
+                .attr('points', function (d) {
+                    var points = d3.select(this).attr('points').split(' '),
+                        x2 = px2(d, view, 'parent2'),
+                        y2 = py2(d, view, 'parent2');
+
+                    points[1] = x2 + ',' + y2;
+                    return points.join(' ');
+                });
+        },
+
+        _renderIdLabels: function () {
+            this._renderText('id-label', function (d) { return d.id + '..'; }, 14);
+            this._renderText('message-label', function (d) { return d.message; }, 24);
+        },
+
+        _renderText: function(className, getText, delta) {
+            var view = this,
+                existingTexts,
+                newtexts;
+
+            existingTexts = this.commitBox.selectAll('text.' + className)
+                .data(this.commitData, function (d) { return d.id; })
+                .text(getText);
+
+            existingTexts.transition().call(fixIdPosition, view, delta);
+
+            newtexts = existingTexts.enter()
+                .insert('svg:text', ':first-child')
+                .classed(className, true)
+                .text(getText)
+                .call(fixIdPosition, view, delta);
+        },
+
+        _parseTagData: function () {
+            var tagData = [], i,
+                headCommit = null;
+
+            for (i = 0; i < this.commitData.length; i++) {
+                var c = this.commitData[i];
+
+                for (var t = 0; t < c.tags.length; t++) {
+                    var tagName = c.tags[t];
+                    if (tagName.toUpperCase() === 'HEAD') {
+                        headCommit = c;
+                    } else if (this.branches.indexOf(tagName) === -1) {
+                        this.branches.push(tagName);
+                    }
+
+                    tagData.push({name: tagName, commit: c.id});
+                }
+            }
+
+            if (!headCommit) {
+                headCommit = this.getCommit(this.currentBranch);
+                headCommit.tags.push('HEAD');
+                tagData.push({name: 'HEAD', commit: headCommit.id});
+            }
+
+            // find out which commits are not branchless
+
+
+            return tagData;
+        },
+
+        _markBranchlessCommits: function () {
+            var branch, commit, parent, parent2, c, b;
+
+            // first mark every commit as branchless
+            for (c = 0; c < this.commitData.length; c++) {
+                this.commitData[c].branchless = true;
+            }
+
+            for (b = 0; b < this.branches.length; b++) {
+                branch = this.branches[b];
+                if (branch.indexOf('/') === -1) {
+                    commit = this.getCommit(branch);
+                    parent = this.getCommit(commit.parent);
+                    parent2 = this.getCommit(commit.parent2);
+
+                    commit.branchless = false;
+
+                    while (parent) {
+                        parent.branchless = false;
+                        parent = this.getCommit(parent.parent);
+                    }
+
+                    // just in case this is a merge commit
+                    while (parent2) {
+                        parent2.branchless = false;
+                        parent2 = this.getCommit(parent2.parent);
+                    }
+                }
+            }
+
+            this.svg.selectAll('circle.commit').call(applyBranchlessClass);
+            this.svg.selectAll('line.commit-pointer').call(applyBranchlessClass);
+            this.svg.selectAll('polyline.merge-pointer').call(applyBranchlessClass);
+        },
+
+        renderTags: function () {
+            var view = this,
+                tagData = this._parseTagData(),
+                existingTags, newTags;
+
+            existingTags = this.tagBox.selectAll('g.branch-tag')
+                .data(tagData, function (d) { return d.name; });
+
+            existingTags.exit().remove();
+
+            existingTags.select('rect')
+                .transition()
+                .duration(500)
+                .attr('y', function (d) { return tagY(d, view); })
+                .attr('x', function (d) {
+                    var commit = view.getCommit(d.commit),
+                        width = Number(d3.select(this).attr('width'));
+
+                    return commit.cx - (width / 2);
+                });
+
+            existingTags.select('text')
+                .transition()
+                .duration(500)
+                .attr('y', function (d) { return tagY(d, view) + 14; })
+                .attr('x', function (d) {
+                    var commit = view.getCommit(d.commit);
+                    return commit.cx;
+                });
+
+            newTags = existingTags.enter()
+                .append('g')
+                .attr('class', function (d) {
+                    var classes = 'branch-tag';
+                    if (d.name.indexOf('[') === 0 && d.name.indexOf(']') === d.name.length - 1) {
+                        classes += ' git-tag';
+                    } else if (d.name.indexOf('/') >= 0) {
+                        classes += ' remote-branch';
+                    } else if (d.name.toUpperCase() === 'HEAD') {
+                        classes += ' head-tag';
+                    }
+                    return classes;
+                });
+
+            newTags.append('svg:rect')
+                .attr('width', function (d) {
+                    return (d.name.length * 6) + 10;
+                })
+                .attr('height', 20)
+                .attr('y', function (d) { return tagY(d, view); })
+                .attr('x', function (d) {
+                    var commit = view.getCommit(d.commit),
+                        width = Number(d3.select(this).attr('width'));
+
+                    return commit.cx - (width / 2);
+                });
+
+            newTags.append('svg:text')
+                .text(function (d) {
+                    if (d.name.indexOf('[') === 0 && d.name.indexOf(']') === d.name.length - 1)
+                        return d.name.substring(1, d.name.length - 1); 
+                    return d.name; 
+                })
+                .attr('y', function (d) {
+                    return tagY(d, view) + 14;
+                })
+                .attr('x', function (d) {
+                    var commit = view.getCommit(d.commit);
+                    return commit.cx;
+                });
+
+            this._markBranchlessCommits();
+        },
+
+        _setCurrentBranch: function (branch) {
+            var display = this.svg.select('text.current-branch-display'),
+                text = 'Current Branch: ';
+
+            if (branch && branch.indexOf('/') === -1) {
+                text += branch;
+                this.currentBranch = branch;
+            } else {
+                text += ' DETACHED HEAD';
+                this.currentBranch = null;
+            }
+
+            display.text(text);
+        },
+
+        moveTag: function (tag, ref) {
+            var currentLoc = this.getCommit(tag),
+                newLoc = this.getCommit(ref);
+
+            if (currentLoc) {
+                currentLoc.tags.splice(currentLoc.tags.indexOf(tag), 1);
+            }
+
+            newLoc.tags.push(tag);
+            return this;
+        },
+
+        /**
+         * @method isAncestor
+         * @param ref1
+         * @param ref2
+         * @return {Boolean} whether or not ref1 is an ancestor of ref2
+         */
+        isAncestor: function isAncestor(ref1, ref2) {
+            var currentCommit = this.getCommit(ref1),
+                targetTree = this.getCommit(ref2),
+                inTree = false,
+                additionalTrees = [];
+
+            if (!currentCommit) {
+                return false;
+            }
+
+            while (targetTree) {
+                if (targetTree.id === currentCommit.id) {
+                    inTree = true;
+                    targetTree = null;
+                } else {
+                    if (targetTree.parent2) {
+                        additionalTrees.push(targetTree.parent2);
+                    }
+                    targetTree = this.getCommit(targetTree.parent);
+                }
+            }
+
+            if (inTree) {
+                return true;
+            }
+
+            for (var i = 0; i < additionalTrees.length; i++) {
+                inTree = isAncestor.call(this, currentCommit, additionalTrees[i]);
+                if (inTree) break;
+            }
+
+            return inTree;
+        },
+
+        commit: function (commit, message) {
+            commit = commit || {};
+
+            !commit.id && (commit.id = HistoryView.generateId());
+            !commit.tags && (commit.tags = []);
+
+            commit.message = message;
+            if (!commit.parent) {
+                if (!this.currentBranch) {
+                    throw new Error('Not a good idea to make commits while in a detached HEAD state.');
+                }
+
+                commit.parent = this.getCommit(this.currentBranch).id;
+            }
+
+            this.commitData.push(commit);
+            this.moveTag(this.currentBranch, commit.id);
+
+            this.renderCommits();
+
+            this.checkout(this.currentBranch);
+            return this;
+        },
+
+        branch: function (name) {
+            if (!name || name.trim() === '') {
+                throw new Error('You need to give a branch name.');
+            }
+
+            if (name === 'HEAD') {
+                throw new Error('You cannot name your branch "HEAD".');
+            }
+
+            if (name.indexOf(' ') > -1) {
+                throw new Error('Branch names cannot contain spaces.');
+            }
+
+            if (this.branches.indexOf(name) > -1) {
+                throw new Error('Branch "' + name + '" already exists.');
+            }
+
+            this.getCommit('HEAD').tags.push(name);
+            this.renderTags();
+            return this;
+        },
+
+        tag: function (name) {
+            this.branch('[' + name + ']');
+        },
+
+        deleteBranch: function (name) {
+            var branchIndex,
+                commit;
+
+            if (!name || name.trim() === '') {
+                throw new Error('You need to give a branch name.');
+            }
+
+            if (name === this.currentBranch) {
+                throw new Error('Cannot delete the currently checked-out branch.');
+            }
+
+            branchIndex = this.branches.indexOf(name);
+
+            if (branchIndex === -1) {
+                throw new Error('That branch doesn\'t exist.');
+            }
+
+            this.branches.splice(branchIndex, 1);
+            commit = this.getCommit(name);
+            branchIndex = commit.tags.indexOf(name);
+
+            if (branchIndex > -1) {
+                commit.tags.splice(branchIndex, 1);
+            }
+
+            this.renderTags();
+        },
+
+        checkout: function (ref) {
+            var commit = this.getCommit(ref);
+
+            if (!commit) {
+                throw new Error('Cannot find commit: ' + ref);
+            }
+
+            var previousHead = this.getCircle('HEAD'),
+                newHead = this.getCircle(commit.id);
+
+            if (previousHead && !previousHead.empty()) {
+                previousHead.classed('checked-out', false);
+            }
+
+            this._setCurrentBranch(ref === commit.id ? null : ref);
+            this.moveTag('HEAD', commit.id);
+            this.renderTags();
+
+            newHead.classed('checked-out', true);
+
+            return this;
+        },
+
+        reset: function (ref) {
+            var commit = this.getCommit(ref);
+
+            if (!commit) {
+                throw new Error('Cannot find ref: ' + ref);
+            }
+
+            if (this.currentBranch) {
+                this.moveTag(this.currentBranch, commit.id);
+                this.checkout(this.currentBranch);
+            } else {
+                this.checkout(commit.id);
+            }
+
+            return this;
+        },
+
+        revert: function (ref) {
+            var commit = this.getCommit(ref);
+
+            if (!commit) {
+                throw new Error('Cannot find ref: ' + ref);
+            }
+
+            if (this.isAncestor(commit, 'HEAD')) {
+                commit.reverted = true;
+                this.commit({reverts: commit.id});
+            } else {
+                throw new Error(ref + 'is not an ancestor of HEAD.');
+            }
+        },
+
+        fastForward: function (ref) {
+            var targetCommit = this.getCommit(ref);
+
+            if (this.currentBranch) {
+                this.moveTag(this.currentBranch, targetCommit.id);
+                this.checkout(this.currentBranch);
+            } else {
+                this.checkout(targetCommit.id);
+            }
+        },
+
+        merge: function (ref, noFF) {
+            var mergeTarget = this.getCommit(ref),
+                currentCommit = this.getCommit('HEAD');
+
+            if (!mergeTarget) {
+                throw new Error('Cannot find ref: ' + ref);
+            }
+
+            if (currentCommit.id === mergeTarget.id) {
+                throw new Error('Already up-to-date.');
+            } else if (currentCommit.parent2 === mergeTarget.id) {
+                throw new Error('Already up-to-date.');
+            } else if (noFF === true) {
+                var branchStartCommit = this.getCommit(mergeTarget.parent);
+                while (branchStartCommit.parent !== currentCommit.id) {
+                    branchStartCommit = this.getCommit(branchStartCommit.parent);
+                }
+                
+                branchStartCommit.isNoFFBranch = true;
+                
+                this.commit({parent2: mergeTarget.id, isNoFFCommit: true});
+            } else if (this.isAncestor(currentCommit, mergeTarget)) {
+                this.fastForward(mergeTarget);
+                return 'Fast-Forward';
+            } else {
+                this.commit({parent2: mergeTarget.id});
+            }
+        },
+
+        rebase: function (ref) {
+            var rebaseTarget = this.getCommit(ref),
+                currentCommit = this.getCommit('HEAD'),
+                isCommonAncestor,
+                rebaseTreeLoc,
+                rebaseMessage,
+                toRebase = [], rebasedCommit,
+                remainingHusk;
+
+            if (!rebaseTarget) {
+                throw new Error('Cannot find ref: ' + ref);
+            }
+
+            if (currentCommit.id === rebaseTarget.id) {
+                throw new Error('Already up-to-date.');
+            } else if (currentCommit.parent2 === rebaseTarget.id) {
+                throw new Error('Already up-to-date.');
+            }
+
+            isCommonAncestor = this.isAncestor(currentCommit, rebaseTarget);
+
+            if (isCommonAncestor) {
+                this.fastForward(rebaseTarget);
+                return 'Fast-Forward';
+            }
+
+            rebaseTreeLoc = rebaseTarget.id;
+
+            while (!isCommonAncestor) {
+                toRebase.unshift(currentCommit);
+                currentCommit = this.getCommit(currentCommit.parent);
+                isCommonAncestor = this.isAncestor(currentCommit, rebaseTarget);
+            }
+
+            for (var i = 0; i < toRebase.length; i++) {
+                rebasedCommit = toRebase[i];
+                rebaseMessage = rebasedCommit.message;
+
+                remainingHusk = {
+                    id: rebasedCommit.id,
+                    parent: rebasedCommit.parent,
+                    message: rebasedCommit.message,
+                    tags: []
+                };
+
+                for (var t = 0; t < rebasedCommit.tags.length; t++) {
+                    var tagName = rebasedCommit.tags[t];
+                    if (tagName !== this.currentBranch && tagName !== 'HEAD') {
+                        remainingHusk.tags.unshift(tagName);
+                    }
+                }
+
+                this.commitData.push(remainingHusk);
+
+                rebasedCommit.parent = rebaseTreeLoc;
+                rebaseTreeLoc = HistoryView.generateId()
+                rebasedCommit.id = rebaseTreeLoc;
+                rebasedCommit.message = rebaseMessage;
+                rebasedCommit.tags.length = 0;
+                rebasedCommit.rebased = true;
+            }
+
+            if (this.currentBranch) {
+                rebasedCommit.tags.push(this.currentBranch);
+            }
+
+            this.renderCommits();
+
+            if (this.currentBranch) {
+                this.checkout(this.currentBranch);
+            } else {
+                this.checkout(rebasedCommit.id);
+            }
+        }
+    };
+
+    return HistoryView;
+});
diff --git a/explain-git-with-d3/js/main.js b/explain-git-with-d3/js/main.js
new file mode 100644 (file)
index 0000000..40e03d7
--- /dev/null
@@ -0,0 +1,55 @@
+if (!String.prototype.trim) {\r
+    String.prototype.trim = function () {\r
+      return this.replace(/^\s+|\s+$/g,'');\r
+    };\r
+}\r
+\r
+if (!Array.isArray) {\r
+    Array.isArray = function (vArg) {\r
+      return Object.prototype.toString.call(vArg) === "[object Array]";\r
+    };\r
+}\r
+\r
+if (!Array.prototype.indexOf) {\r
+    Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {\r
+        "use strict";\r
+        if (this == null) {\r
+            throw new TypeError();\r
+        }\r
+        var t = Object(this);\r
+        var len = t.length >>> 0;\r
+        if (len === 0) {\r
+            return -1;\r
+        }\r
+        var n = 0;\r
+        if (arguments.length > 1) {\r
+            n = Number(arguments[1]);\r
+            if (n != n) { // shortcut for verifying if it's NaN\r
+                n = 0;\r
+            } else if (n != 0 && n != Infinity && n != -Infinity) {\r
+                n = (n > 0 || -1) * Math.floor(Math.abs(n));\r
+            }\r
+        }\r
+        if (n >= len) {\r
+            return -1;\r
+        }\r
+        var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);\r
+        for (; k < len; k++) {\r
+            if (k in t && t[k] === searchElement) {\r
+                return k;\r
+            }\r
+        }\r
+        return -1;\r
+    }\r
+}\r
+\r
+require.config({\r
+    paths: {\r
+        'd3': 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min'\r
+    },\r
+    shim: {\r
+        'd3': {\r
+            exports: 'd3'\r
+        }\r
+    }\r
+});
\ No newline at end of file
diff --git a/explain-git-with-d3/js/require.min.js b/explain-git-with-d3/js/require.min.js
new file mode 100644 (file)
index 0000000..d58ca20
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ RequireJS 2.1.4 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ Available via the MIT or new BSD license.
+ see: http://github.com/jrburke/requirejs for details
+*/
+var requirejs,require,define;
+(function(Y){function I(b){return"[object Function]"===L.call(b)}function J(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function M(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function r(b,c){return da.call(b,c)}function i(b,c){return r(b,c)&&b[c]}function E(b,c){for(var d in b)if(r(b,d)&&c(b[d],d))break}function Q(b,c,d,i){c&&E(c,function(c,h){if(d||!r(b,h))i&&"string"!==typeof c?(b[h]||(b[h]={}),Q(b[h],
+c,d,i)):b[h]=c});return b}function t(b,c){return function(){return c.apply(b,arguments)}}function Z(b){if(!b)return b;var c=Y;x(b.split("."),function(b){c=c[b]});return c}function F(b,c,d,i){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=i;d&&(c.originalError=d);return c}function ea(b){function c(a,f,v){var e,n,b,c,d,k,g,h=f&&f.split("/");e=h;var l=m.map,j=l&&l["*"];if(a&&"."===a.charAt(0))if(f){e=i(m.pkgs,f)?h=[f]:h.slice(0,h.length-1);f=a=e.concat(a.split("/"));
+for(e=0;f[e];e+=1)if(n=f[e],"."===n)f.splice(e,1),e-=1;else if(".."===n)if(1===e&&(".."===f[2]||".."===f[0]))break;else 0<e&&(f.splice(e-1,2),e-=2);e=i(m.pkgs,f=a[0]);a=a.join("/");e&&a===f+"/"+e.main&&(a=f)}else 0===a.indexOf("./")&&(a=a.substring(2));if(v&&(h||j)&&l){f=a.split("/");for(e=f.length;0<e;e-=1){b=f.slice(0,e).join("/");if(h)for(n=h.length;0<n;n-=1)if(v=i(l,h.slice(0,n).join("/")))if(v=i(v,b)){c=v;d=e;break}if(c)break;!k&&(j&&i(j,b))&&(k=i(j,b),g=e)}!c&&k&&(c=k,d=g);c&&(f.splice(0,d,
+c),a=f.join("/"))}return a}function d(a){z&&x(document.getElementsByTagName("script"),function(f){if(f.getAttribute("data-requiremodule")===a&&f.getAttribute("data-requirecontext")===k.contextName)return f.parentNode.removeChild(f),!0})}function y(a){var f=i(m.paths,a);if(f&&J(f)&&1<f.length)return d(a),f.shift(),k.require.undef(a),k.require([a]),!0}function g(a){var f,b=a?a.indexOf("!"):-1;-1<b&&(f=a.substring(0,b),a=a.substring(b+1,a.length));return[f,a]}function h(a,f,b,e){var n,u,d=null,h=f?f.name:
+null,l=a,m=!0,j="";a||(m=!1,a="_@r"+(L+=1));a=g(a);d=a[0];a=a[1];d&&(d=c(d,h,e),u=i(p,d));a&&(d?j=u&&u.normalize?u.normalize(a,function(a){return c(a,h,e)}):c(a,h,e):(j=c(a,h,e),a=g(j),d=a[0],j=a[1],b=!0,n=k.nameToUrl(j)));b=d&&!u&&!b?"_unnormalized"+(M+=1):"";return{prefix:d,name:j,parentMap:f,unnormalized:!!b,url:n,originalName:l,isDefine:m,id:(d?d+"!"+j:j)+b}}function q(a){var f=a.id,b=i(j,f);b||(b=j[f]=new k.Module(a));return b}function s(a,f,b){var e=a.id,n=i(j,e);if(r(p,e)&&(!n||n.defineEmitComplete))"defined"===
+f&&b(p[e]);else q(a).on(f,b)}function A(a,f){var b=a.requireModules,e=!1;if(f)f(a);else if(x(b,function(f){if(f=i(j,f))f.error=a,f.events.error&&(e=!0,f.emit("error",a))}),!e)l.onError(a)}function w(){R.length&&(fa.apply(G,[G.length-1,0].concat(R)),R=[])}function B(a,f,b){var e=a.map.id;a.error?a.emit("error",a.error):(f[e]=!0,x(a.depMaps,function(e,c){var d=e.id,h=i(j,d);h&&(!a.depMatched[c]&&!b[d])&&(i(f,d)?(a.defineDep(c,p[d]),a.check()):B(h,f,b))}),b[e]=!0)}function C(){var a,f,b,e,n=(b=1E3*m.waitSeconds)&&
+k.startTime+b<(new Date).getTime(),c=[],h=[],g=!1,l=!0;if(!T){T=!0;E(j,function(b){a=b.map;f=a.id;if(b.enabled&&(a.isDefine||h.push(b),!b.error))if(!b.inited&&n)y(f)?g=e=!0:(c.push(f),d(f));else if(!b.inited&&(b.fetched&&a.isDefine)&&(g=!0,!a.prefix))return l=!1});if(n&&c.length)return b=F("timeout","Load timeout for modules: "+c,null,c),b.contextName=k.contextName,A(b);l&&x(h,function(a){B(a,{},{})});if((!n||e)&&g)if((z||$)&&!U)U=setTimeout(function(){U=0;C()},50);T=!1}}function D(a){r(p,a[0])||
+q(h(a[0],null,!0)).init(a[1],a[2])}function H(a){var a=a.currentTarget||a.srcElement,b=k.onScriptLoad;a.detachEvent&&!V?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=k.onScriptError;(!a.detachEvent||V)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function K(){var a;for(w();G.length;){a=G.shift();if(null===a[0])return A(F("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));D(a)}}var T,W,k,N,U,m={waitSeconds:7,
+baseUrl:"./",paths:{},pkgs:{},shim:{},map:{},config:{}},j={},X={},G=[],p={},S={},L=1,M=1;N={require:function(a){return a.require?a.require:a.require=k.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=p[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m.config&&i(m.config,a.map.id)||{}},exports:p[a.map.id]}}};W=function(a){this.events=i(X,a.id)||{};this.map=a;this.shim=
+i(m.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};W.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=t(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=
+b)},fetch:function(){if(!this.fetched){this.fetched=!0;k.startTime=(new Date).getTime();var a=this.map;if(this.shim)k.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],t(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;S[a]||(S[a]=!0,k.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,n=this.factory;
+if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(I(n)){if(this.events.error)try{e=k.execCb(c,n,b,e)}catch(d){a=d}else e=k.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",A(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&
+!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(k,this.map,this.depMaps);delete j[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,g=k.makeRequire(a.parentMap,{enableBuildCallback:!0});
+if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(j,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error=a;a.requireModules=
+[b];E(j,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete j[a.map.id]});A(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(j){return A(F("fromtexteval","fromText eval for "+b+" failed: "+j,j,[b]))}v&&(O=!0);this.depMaps.push(u);k.completeLoad(d);g([d],n)}),e.load(a.name,g,n,m)}));k.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a,
+b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=j[c];!r(N,c)&&(e&&!e.enabled)&&k.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(j,a.id);b&&!b.enabled&&k.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=
+this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};k={config:m,contextName:b,registry:j,defined:p,urlFetched:S,defQueue:G,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a,
+b){J(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=k.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(j,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)k.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments));
+return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function g(e,c,u){var i,m;d.enableBuildCallback&&(c&&I(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(I(c))return A(F("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](j[a.id]);if(l.get)return l.get(k,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?A(F("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();k.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap;
+m.init(e,c,u,{enabled:!0});C()});return g}d=d||{};Q(g,{isBrowser:z,toUrl:function(b){var d,f=b.lastIndexOf("."),h=b.split("/")[0];if(-1!==f&&(!("."===h||".."===h)||1<f))d=b.substring(f,b.length),b=b.substring(0,f);b=k.nameToUrl(c(b,a&&a.id,!0),d||".fake");return d?b:b.substring(0,b.length-5)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(j,b)}});a||(g.undef=function(b){w();var c=h(b,a,!0),d=i(j,b);delete p[b];delete S[c.url];delete X[b];
+d&&(d.events.defined&&(X[b]=d.events),delete j[b])});return g},enable:function(a){i(j,a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();G.length;){c=G.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(j,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:A(F("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}C()},nameToUrl:function(a,b){var c,d,h,g,k,j;if(l.jsExtRegExp.test(a))g=
+a+(b||"");else{c=m.paths;d=m.pkgs;g=a.split("/");for(k=g.length;0<k;k-=1)if(j=g.slice(0,k).join("/"),h=i(d,j),j=i(c,j)){J(j)&&(j=j[0]);g.splice(0,k,j);break}else if(h){c=a===h.name?h.location+"/"+h.main:h.location;g.splice(0,k,c);break}g=g.join("/");g+=b||(/\?/.test(g)?"":".js");g=("/"===g.charAt(0)||g.match(/^[\w\+\.\-]+:/)?"":m.baseUrl)+g}return m.urlArgs?g+((-1===g.indexOf("?")?"?":"&")+m.urlArgs):g},load:function(a,b){l.load(k,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===
+a.type||ha.test((a.currentTarget||a.srcElement).readyState))P=null,a=H(a),k.completeLoad(a.id)},onScriptError:function(a){var b=H(a);if(!y(b.id))return A(F("scripterror","Script error",a,[b.id]))}};k.require=k.makeRequire();return k}var l,w,B,D,s,H,P,K,ba,ca,ia=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ja=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,aa=/\.js$/,ga=/^\.\//;w=Object.prototype;var L=w.toString,da=w.hasOwnProperty,fa=Array.prototype.splice,z=!!("undefined"!==typeof window&&navigator&&
+document),$=!z&&"undefined"!==typeof importScripts,ha=z&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,V="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),C={},q={},R=[],O=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(I(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!I(require)&&(q=require,require=void 0);l=requirejs=function(b,c,d,y){var g,h="_";!J(b)&&"string"!==typeof b&&(g=b,J(c)?(b=c,c=d,d=y):b=[]);
+g&&g.context&&(h=g.context);(y=i(C,h))||(y=C[h]=l.s.newContext(h));g&&y.configure(g);return y.require(b,c,d)};l.config=function(b){return l(b)};l.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=l);l.version="2.1.4";l.jsExtRegExp=/^\/|:|\?|\.js$/;l.isBrowser=z;w=l.s={contexts:C,newContext:ea};l({});x(["toUrl","undef","defined","specified"],function(b){l[b]=function(){var c=C._;return c.require[b].apply(c,arguments)}});if(z&&(B=w.head=document.getElementsByTagName("head")[0],
+D=document.getElementsByTagName("base")[0]))B=w.head=D.parentNode;l.onError=function(b){throw b;};l.load=function(b,c,d){var i=b&&b.config||{},g;if(z)return g=i.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),g.type=i.scriptType||"text/javascript",g.charset="utf-8",g.async=!0,g.setAttribute("data-requirecontext",b.contextName),g.setAttribute("data-requiremodule",c),g.attachEvent&&!(g.attachEvent.toString&&0>g.attachEvent.toString().indexOf("[native code"))&&
+!V?(O=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,K=g,D?B.insertBefore(g,D):B.appendChild(g),K=null,g;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){B||(B=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(H=s.split("/"),ba=H.pop(),ca=H.length?H.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):
+[s],!0});define=function(b,c,d){var i,g;"string"!==typeof b&&(d=c,c=b,b=null);J(c)||(d=c,c=[]);!c.length&&I(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),g=C[i.getAttribute("data-requirecontext")])}(g?
+g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this);
diff --git a/explain-git-with-d3/memtest.html b/explain-git-with-d3/memtest.html
new file mode 100644 (file)
index 0000000..bfd6718
--- /dev/null
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Explain Git with D3</title>
+<script data-main="js/main" src="http://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.4/require.min.js"></script>
+<link rel="stylesheet" href="css/explaingit.css">
+</head>
+<body>
+<h1>Memtest Page</h1>
+<p>This page exists to help me find any memory leaks that may happen.</p>
+
+<h2>explain git memtest</h2>
+<div id="ExplainGitMemtest-Container" class="concept-container">
+    <p>
+        Create and destroy many git history views and control boxes to find memory leaks.
+    </p>
+</div>
+<h3><a id="start-test" href="#">Start Test</a></h3>
+<script type="text/javascript">
+require(['explaingit'], function (explainGit) {
+    var open = function () {
+        openSwitch.style.display = 'none';
+
+        explainGit.open({
+            name: 'Memtest',
+            height: 250,
+            commitData: [
+                {id: '98ca9b5', tags: ['origin/master', 'master']}
+            ]
+        });
+    };
+
+    document.getElementById('start-test').addEventListener('click', function () {
+        for (var i = 0; i < 100; i++) {
+            open();
+            explainGit.reset();
+            console.log('test run: ' + i);
+        }
+    }, false);
+});
+</script>
+<h3><a href="index.html">Back to Home</a></h3>
+</body>
+</html>
\ No newline at end of file