You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by sa...@apache.org on 2021/12/02 18:12:50 UTC

[atlas] branch branch-2.0 updated (8810c44 -> ee0d37b)

This is an automated email from the ASF dual-hosted git repository.

sarath pushed a change to branch branch-2.0
in repository https://gitbox.apache.org/repos/asf/atlas.git.


    from 8810c44  ATLAS-4473: GlossaryTerms Bulk Create Performance Improvement
     new 447efb8  ATLAS-4370: Persist Metrics for user to retrieve Metrics info at a past timestamp
     new ee0d37b  ATLAS-4451:UI: #2 Chart representation of Metrics data on Statistics dialog box

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 addons/models/0000-Area0/0010-base_model.json      |    42 +
 dashboardv2/public/css/nv.d3.min.css               |    18 +
 dashboardv2/public/css/scss/stats.scss             |    77 +
 dashboardv2/public/index.html.tpl                  |     1 +
 dashboardv2/public/js/router/Router.js             |     2 +-
 ...ics_tmpl.html => Statistics_Timeline_tmlp.html} |    38 +-
 .../public/js/templates/site/entity_tmpl.html      |     2 +-
 dashboardv2/public/js/utils/UrlLinks.js            |     9 +
 .../{Statistics.js => StatisticsTimelineView.js}   |   354 +-
 dashboardv2/public/js/views/site/d3.v3.min.js      |    23 +
 dashboardv2/public/js/views/site/nv.d3.js          | 18144 +++++++++++++++++++
 dashboardv3/public/css/nv.d3.min.css               |    18 +
 dashboardv3/public/css/scss/stats.scss             |    77 +
 dashboardv3/public/index.html.tpl                  |     1 +
 dashboardv3/public/js/router/Router.js             |     2 +-
 .../templates/site/Statistics_Timeline_tmlp.html   |    38 +-
 .../public/js/templates/site/entity_tmpl.html      |     2 +-
 dashboardv3/public/js/utils/UrlLinks.js            |     9 +
 .../public/js/views/site/StatisticsTimelineView.js |   354 +-
 dashboardv3/public/js/views/site/d3.v3.min.js      |    23 +
 dashboardv3/public/js/views/site/nv.d3.js          | 18144 +++++++++++++++++++
 .../java/org/apache/atlas/AtlasConfiguration.java  |     4 +-
 .../main/java/org/apache/atlas/AtlasErrorCode.java |     1 +
 .../AtlasMetricsMapToChart.java}                   |    48 +-
 .../atlas/model/metrics/AtlasMetricsStat.java      |   159 +
 .../org/apache/atlas/utils/AtlasEntityUtil.java    |    50 +
 .../repository/graph/GraphBackedSearchIndexer.java |     7 +
 .../ogm/metrics/AtlasMetricsStatDTO.java           |   153 +
 .../org/apache/atlas/services/MetricsService.java  |   264 +-
 .../test/java/org/apache/atlas/TestModules.java    |     2 +
 .../apache/atlas/services/MetricsServiceTest.java  |    78 +-
 .../resources/solr/core-template/solrconfig.xml    |     2 +-
 .../apache/atlas/web/resources/AdminResource.java  |   196 +-
 33 files changed, 38245 insertions(+), 97 deletions(-)
 create mode 100644 dashboardv2/public/css/nv.d3.min.css
 copy dashboardv2/public/js/templates/site/{Statistics_tmpl.html => Statistics_Timeline_tmlp.html} (80%)
 copy dashboardv2/public/js/views/site/{Statistics.js => StatisticsTimelineView.js} (57%)
 create mode 100644 dashboardv2/public/js/views/site/d3.v3.min.js
 create mode 100644 dashboardv2/public/js/views/site/nv.d3.js
 create mode 100644 dashboardv3/public/css/nv.d3.min.css
 copy dashboardv2/public/js/templates/site/Statistics_tmpl.html => dashboardv3/public/js/templates/site/Statistics_Timeline_tmlp.html (80%)
 copy dashboardv2/public/js/views/site/Statistics.js => dashboardv3/public/js/views/site/StatisticsTimelineView.js (57%)
 create mode 100644 dashboardv3/public/js/views/site/d3.v3.min.js
 create mode 100644 dashboardv3/public/js/views/site/nv.d3.js
 copy intg/src/main/java/org/apache/atlas/model/{discovery/AtlasSuggestionsResult.java => metrics/AtlasMetricsMapToChart.java} (57%)
 create mode 100644 intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsStat.java
 create mode 100644 repository/src/main/java/org/apache/atlas/repository/ogm/metrics/AtlasMetricsStatDTO.java

[atlas] 02/02: ATLAS-4451:UI: #2 Chart representation of Metrics data on Statistics dialog box

Posted by sa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sarath pushed a commit to branch branch-2.0
in repository https://gitbox.apache.org/repos/asf/atlas.git

commit ee0d37b03e3844c33449dfef18f6fff26ca125e2
Author: prasad pawar <pr...@freestoneinfotech.com>
AuthorDate: Tue Nov 30 13:00:13 2021 +0530

    ATLAS-4451:UI: #2 Chart representation of Metrics data on Statistics dialog box
    
    Signed-off-by: Sarath Subramanian <sa...@apache.org>
    (cherry picked from commit bc2e13cd119663f3e827fb05e4a5d5d25026b411)
---
 dashboardv2/public/css/nv.d3.min.css               |    18 +
 dashboardv2/public/css/scss/stats.scss             |    77 +
 dashboardv2/public/index.html.tpl                  |     1 +
 dashboardv2/public/js/router/Router.js             |     2 +-
 .../templates/site/Statistics_Timeline_tmlp.html   |   223 +
 .../public/js/templates/site/entity_tmpl.html      |     2 +-
 dashboardv2/public/js/utils/UrlLinks.js            |     9 +
 .../public/js/views/site/StatisticsTimelineView.js |   815 +
 dashboardv2/public/js/views/site/d3.v3.min.js      |    23 +
 dashboardv2/public/js/views/site/nv.d3.js          | 18144 +++++++++++++++++++
 dashboardv3/public/css/nv.d3.min.css               |    18 +
 dashboardv3/public/css/scss/stats.scss             |    77 +
 dashboardv3/public/index.html.tpl                  |     1 +
 dashboardv3/public/js/router/Router.js             |     2 +-
 .../templates/site/Statistics_Timeline_tmlp.html   |   223 +
 .../public/js/templates/site/entity_tmpl.html      |     2 +-
 dashboardv3/public/js/utils/UrlLinks.js            |     9 +
 .../public/js/views/site/StatisticsTimelineView.js |   815 +
 dashboardv3/public/js/views/site/d3.v3.min.js      |    23 +
 dashboardv3/public/js/views/site/nv.d3.js          | 18144 +++++++++++++++++++
 20 files changed, 38624 insertions(+), 4 deletions(-)

diff --git a/dashboardv2/public/css/nv.d3.min.css b/dashboardv2/public/css/nv.d3.min.css
new file mode 100644
index 0000000..8dcede1
--- /dev/null
+++ b/dashboardv2/public/css/nv.d3.min.css
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.nvd3 .nv-axis line,.nvd3 .nv-axis path{fill:none;shape-rendering:crispEdges}.nv-brush .extent,.nvd3 .background path,.nvd3 .nv-axis line,.nvd3 .nv-axis path{shape-rendering:crispEdges}.nv-distx,.nv-disty,.nv-noninteractive,.nvd3 .nv-axis,.nvd3.nv-pie .nv-label,.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3 .nv-axis{opacity:1}.nvd3 .nv-axis.nv-disabled,.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3 .nv-axis path{stroke:#000;stroke-opacity:.75} [...]
+/*# sourceMappingURL=nv.d3.min.css.map */
\ No newline at end of file
diff --git a/dashboardv2/public/css/scss/stats.scss b/dashboardv2/public/css/scss/stats.scss
index 7bf7581..f0eca3e 100644
--- a/dashboardv2/public/css/scss/stats.scss
+++ b/dashboardv2/public/css/scss/stats.scss
@@ -170,4 +170,81 @@
             padding: 10px 10px;
         }
     }
+}
+
+.statistics-time-list {
+    padding-right: 0px;
+    width: 230px;
+    float: right;
+}
+
+.entityTable {
+    tbody {
+        border-bottom: 2px solid #ddd;
+    }
+}
+
+.table-row-selected {
+    background-color: #F5F5F5;
+
+    td span {
+        color: $color_jungle_green_approx;
+    }
+}
+
+.entity-type-chart {
+    cursor: pointer;
+}
+
+.entity-chart-panel {
+    margin: 0px !important;
+    padding: 0px !important;
+    border: none;
+
+    .panel-body {
+        padding: 0px;
+    }
+}
+
+.chart-container {
+    margin-top: 20px;
+
+    .chart-heading {
+        display: flex;
+
+        .chart-title {
+            width: 50%;
+            padding: 8px;
+            font-weight: bold;
+        }
+
+        .chart-buttons-container {
+            width: 50%;
+
+            ul {
+                display: flex;
+
+                li {
+                    padding: 8px 0px 8px 0px;
+                    margin: 0px 3px 0px 3px;
+                    cursor: pointer;
+
+                    a {
+                        color: $color_ironside_gray_approx;
+                    }
+                }
+
+                .active a {
+                    color: $color_jungle_green_approx;
+                }
+            }
+
+        }
+    }
+
+    .chart-svg {
+        width: 725px;
+        height: 300px;
+        margin: 0 auto;
+    }
 }
\ No newline at end of file
diff --git a/dashboardv2/public/index.html.tpl b/dashboardv2/public/index.html.tpl
index 55cf2e8..f678fd9 100644
--- a/dashboardv2/public/index.html.tpl
+++ b/dashboardv2/public/index.html.tpl
@@ -59,6 +59,7 @@
     <link rel="stylesheet" href="js/libs/jquery-asBreadcrumbs/css/asBreadcrumbs.min.css?bust=<%- bust %>">
     <link rel="stylesheet" href="css/googlefonts.css?bust=<%- bust %>" type='text/css'>
     <link rel="stylesheet" type="text/css" href="js/external_lib/jquery-ui/jquery-ui.min.css?bust=<%- bust %>">
+    <link rel="stylesheet" href="css/nv.d3.min.css?bust=<%- bust %>" >
     <link href="css/bootstrap-sidebar.css?bust=<%- bust %>" rel="stylesheet">
     <link href="js/libs/font-awesome/css/font-awesome.min.css?bust=<%- bust %>" rel="stylesheet">
     <link href="js/external_lib/pnotify/pnotify.custom.min.css?bust=<%- bust %>" rel="stylesheet">
diff --git a/dashboardv2/public/js/router/Router.js b/dashboardv2/public/js/router/Router.js
index ea26bf5..b3b0b96 100644
--- a/dashboardv2/public/js/router/Router.js
+++ b/dashboardv2/public/js/router/Router.js
@@ -92,7 +92,7 @@ define([
             var that = this;
             $('body').on('click', 'a.show-stat', function() {
                 require([
-                    'views/site/Statistics',
+                    'views/site/StatisticsTimelineView',
                 ], function(Statistics) {
                     new Statistics(_.extend({}, that.preFetchedCollectionLists, that.sharedObj, that.ventObj));
                 });
diff --git a/dashboardv2/public/js/templates/site/Statistics_Timeline_tmlp.html b/dashboardv2/public/js/templates/site/Statistics_Timeline_tmlp.html
new file mode 100644
index 0000000..09f76a0
--- /dev/null
+++ b/dashboardv2/public/js/templates/site/Statistics_Timeline_tmlp.html
@@ -0,0 +1,223 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+<div class="statistics-header col-md-12 hide">
+    <div class="progress hide">
+        <div class="progress-bar progress-bar-success progress-bar-striped" role="progressbar" aria-valuemin="0" aria-valuemax="100" data-id="migrationProgressBar">
+            <span data-id="migrationProgressBarValue"></span>
+        </div>
+    </div>
+    <pre class="successStatus hide">Migration is <span class="color">successful</span>. To exit form migration mode, please remove <span class="color">atlas.migration.data.filename</span> property and restart Atlas.</pre>
+    <div class="refresh-container statisticsRefresh" data-id="statisticsRefresh">
+        <button type="button" class="btn fa fa-refresh pull-right btn-sm btn-action" title="Refresh Data"></button>
+    </div>
+</div>
+<div class="statistics-timeline col-sm-12">
+    <div class="form-group clearfix">
+        <div class="col-sm-4 pull-right" style="padding:0px;">
+            <select class="form-control statistics-time-list" data-id="metricsTimeList"></select>
+        </div>
+    </div>
+</div>
+<div class="panel-group server-stats-container statsContainer hide col-md-12" id="accordion">
+    <div class="modalOverlay" style="position: absolute;width:100%;height: 100%;cursor: wait;z-index: 1;"></div>
+    <div class="panel panel-default custom-panel expand_collapse_panel-icon" data-id="entity">
+        <div class="panel-heading" data-toggle="collapse" href="#entityCountCollapse" aria-expanded="true">
+            <h4 class="panel-title">
+                <a>Entities <span class="count">(0)</span></a>
+            </h4>
+            <div class="btn-group pull-left">
+                <button type="button" title="Collapse"><i class="ec-icon fa"></i></button>
+            </div>
+        </div>
+        <div id="entityCountCollapse" class="panel-collapse collapse in">
+            <div class="panel-body">
+                <table class="table table-hover stat-table entityTable" data-id="entity-card">
+                    <tbody>
+                        <tr class="empty text-center">
+                            <td colspan="3"><span>No records found!</span></td>
+                        </tr>
+                    </tbody>
+                </table>
+                <div class="panel panel-default custom-panel expand_collapse_panel-icon entity-chart-panel" data-id="entity">
+                   <!--  <div class="panel-heading" data-toggle="collapse" href="#entityChartCollapse" aria-expanded="true" data-id="entityChartCollapseHeading">
+                        <h4 class="panel-title">
+                            <a>Trending Chart</a>
+                        </h4>
+                        <div class="btn-group pull-left">
+                            <button type="button" title="Collapse"><i class="ec-icon fa"></i></button>
+                        </div>
+                    </div> -->
+                    <div id="entityChartCollapse" data-id="entityChartCollapse" class="panel-collapse collapse in">
+                        <div class="panel-body">
+                            <div class="chart-container">
+                                <div class="chart-heading">
+                                    <div class="chart-title">
+                                        <span data-id="chartTitle"></span>
+                                    </div>
+                                    <div class="chart-buttons-container" data-id="chartTimeLineList">
+                                    </div>
+                                </div>
+                                <div class="col-sm-12 bar-stacked-graph">
+                                    <div id="compareGraph">
+                                        <svg data-id="compareGraph" style="width:725px; height:300px;margin: 0 auto;"></svg>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="panel panel-default custom-panel expand_collapse_panel-icon" data-id="classification">
+        <div class="panel-heading" data-toggle="collapse" href="#classificationCountCollapse" aria-expanded="true">
+            <h4 class="panel-title">
+                <a>Classifications <span class="count">(0)</span></a>
+            </h4>
+            <div class="btn-group pull-left">
+                <button type="button" title="Collapse"><i class="ec-icon fa"></i></button>
+            </div>
+        </div>
+        <div id="classificationCountCollapse" class="panel-collapse collapse in">
+            <div class="panel-body">
+                <table class="table stat-table classificationTable">
+                    <thead>
+                        <tr>
+                            <th class="">Name</th>
+                            <th class="">Count <span class="count">(0)</span></th>
+                        </tr>
+                    </thead>
+                    <tbody data-id="classification-card">
+                        <tr class="empty text-center">
+                            <td colspan="3"><span>No records found!</span></td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+    <div class="panel panel-default custom-panel expand_collapse_panel-icon" data-id="stats">
+        <div class="panel-heading" data-toggle="collapse" href="#serverDetailcollapse" aria-expanded="true">
+            <h4 class="panel-title">
+                <a>Server Statistics </a>
+            </h4>
+            <div class="btn-group pull-left">
+                <button type="button" title="Collapse"><i class="ec-icon fa"></i></button>
+            </div>
+        </div>
+        <div id="serverDetailcollapse" class="panel-collapse collapse in">
+            <div class="panel-body">
+                <div class="col-sm-12">
+                    <div class="card-container panel panel-primary">
+                        <div class="panel-heading">Server Details</div>
+                        <div class="panel-body">
+                            <table class="table stat-table">
+                                <tbody data-id="server-card">
+                                    <tr class="empty text-center">
+                                        <td colspan="2"><span>No records found!</span></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-sm-12">
+                    <div class="card-container panel panel-primary hide" data-id="notificationDetails">
+                        <div class="panel-heading">Notification Details</div>
+                        <div class="panel-body">
+                            <table data-id="offset-card" class="table stat-table notification-table table-striped ">
+                                <tbody>
+                                    <tr class="empty text-center">
+                                        <td colspan="2"><span>No records found!</span></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                            <hr>
+                            </hr>
+                            <table data-id="notification-card" class="table stat-table notification-table table-striped ">
+                                <tbody>
+                                    <tr class="empty text-center">
+                                        <td colspan="2"><span>No records found!</span></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="panel panel-default custom-panel expand_collapse_panel-icon" data-id="systemDetails">
+        <div class="panel-heading" data-toggle="collapse" href="#systemDetailCollapse" aria-expanded="false">
+            <h4 class="panel-title">
+                <a>System Details</a>
+            </h4>
+            <div class="btn-group pull-left">
+                <button type="button" title="Collapse"><i class="ec-icon fa"></i></button>
+            </div>
+        </div>
+        <div id="systemDetailCollapse" class="panel-collapse collapse">
+            <div class="panel-body">
+                <div class="col-sm-6">
+                    <div class="card-container panel panel-primary">
+                        <div class="panel-heading">OS</div>
+                        <div class="panel-body">
+                            <table class="table stat-table">
+                                <tbody data-id="os-card">
+                                    <tr class="empty text-center">
+                                        <td colspan="2"><span>No records found!</span></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-sm-6">
+                    <div class="card-container panel panel-primary">
+                        <div class="panel-heading">Runtime</div>
+                        <div class="panel-body">
+                            <table class="table stat-table">
+                                <tbody data-id="runtime-card">
+                                    <tr class="empty text-center">
+                                        <td colspan="2"><span>No records found!</span></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-sm-12">
+                    <div class="card-container panel panel-primary">
+                        <div class="panel-heading">Memory</div>
+                        <div class="panel-body">
+                            <table class="table stat-table memory-details">
+                                <tbody data-id="memory-card">
+                                    <tr class="empty text-center">
+                                        <td colspan="2"><span>No records found!</span></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="fontLoader-relative statsLoader show">
+    <i class="fa fa-refresh fa-spin-custom"></i>
+</div>
\ No newline at end of file
diff --git a/dashboardv2/public/js/templates/site/entity_tmpl.html b/dashboardv2/public/js/templates/site/entity_tmpl.html
index ec88fd1..ac6795e 100644
--- a/dashboardv2/public/js/templates/site/entity_tmpl.html
+++ b/dashboardv2/public/js/templates/site/entity_tmpl.html
@@ -25,7 +25,7 @@
 {{#if data}}
 <tbody>
     {{#each data}}
-    <tr>
+    <tr class="entity-type-chart" id={{@key}}>
         <td><span>{{@key}}&nbsp;({{arithmetic this.active '+' this.deleted}})</span></td>
         {{#if this.active}}
         <td>
diff --git a/dashboardv2/public/js/utils/UrlLinks.js b/dashboardv2/public/js/utils/UrlLinks.js
index eb2980c..243b459 100644
--- a/dashboardv2/public/js/utils/UrlLinks.js
+++ b/dashboardv2/public/js/utils/UrlLinks.js
@@ -46,6 +46,15 @@ define(['require', 'utils/Enums', 'utils/Utils', 'underscore'], function(require
         metricsApiUrl: function() {
             return this.baseUrl + '/admin/metrics'
         },
+        metricsAllCollectionTimeApiUrl: function() {
+            return this.baseUrl + '/admin/metricsstats'
+        },
+        metricsCollectionTimeApiUrl: function() {
+            return this.baseUrl + '/admin/metricsstat/'
+        },
+        metricsGraphUrl:function(){
+            return this.baseUrl + '/admin/metricsstats/charts'
+        },
         pendingTaskApiUrl: function() {
             return this.baseUrl + '/admin/tasks'
         },
diff --git a/dashboardv2/public/js/views/site/StatisticsTimelineView.js b/dashboardv2/public/js/views/site/StatisticsTimelineView.js
new file mode 100644
index 0000000..d7caa60
--- /dev/null
+++ b/dashboardv2/public/js/views/site/StatisticsTimelineView.js
@@ -0,0 +1,815 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define(['require',
+    'backbone',
+    'views/site/d3.v3.min',
+    'hbs!tmpl/site/Statistics_Timeline_tmlp',
+    'hbs!tmpl/site/Statistics_Notification_table_tmpl',
+    'hbs!tmpl/site/Statistics_Topic_Offset_table_tmpl',
+    'hbs!tmpl/site/entity_tmpl',
+    'modules/Modal',
+    'models/VCommon',
+    'utils/UrlLinks',
+    'collection/VTagList',
+    'utils/CommonViewFunction',
+    'utils/Enums',
+    'utils/MigrationEnums',
+    'moment',
+    'utils/Utils',
+    'utils/Globals',
+    'views/site/nv.d3',
+    'moment-timezone',
+    'daterangepicker'
+], function(require, Backbone, d3, StatTimelineTmpl, StatsNotiTable, TopicOffsetTable, EntityTable, Modal, VCommon, UrlLinks, VTagList, CommonViewFunction, Enums, MigrationEnums, moment, Utils, Globals) {
+    'use strict';
+
+    var StatisticsTimelineView = Backbone.Marionette.LayoutView.extend(
+        /** @lends AboutAtlasView */
+        {
+            template: StatTimelineTmpl,
+
+            /** Layout sub regions */
+            regions: {},
+            /** ui selector cache */
+            ui: {
+                entity: "[data-id='entity']",
+                classification: "[data-id='classification']",
+                serverCard: "[data-id='server-card']",
+                connectionCard: "[data-id='connection-card']",
+                notificationCard: "[data-id='notification-card']",
+                statsNotificationTable: "[data-id='stats-notification-table']",
+                entityCard: "[data-id='entity-card']",
+                classificationCard: "[data-id='classification-card']",
+                offsetCard: "[data-id='offset-card']",
+                osCard: "[data-id='os-card']",
+                runtimeCard: "[data-id='runtime-card']",
+                memoryCard: "[data-id='memory-card']",
+                memoryPoolUsage: "[data-id='memory-pool-usage-card']",
+                statisticsRefresh: "[data-id='statisticsRefresh']",
+                notificationDetails: "[data-id='notificationDetails']",
+                migrationProgressBar: "[data-id='migrationProgressBar']",
+                migrationProgressBarValue: "[data-id='migrationProgressBarValue']",
+                metricsTimeLine: "[data-id='metricsTimeLine']",
+                metricsTimeList: "[data-id='metricsTimeList']",
+                chartTitle: "[data-id='chartTitle']",
+                chartTimeLineList: "[data-id='chartTimeLineList']",
+                chartButtons: ".chart-buttons",
+                compareGraph: "[data-id='compareGraph']",
+                entityChartCollapseHeading: "[data-id='entityChartCollapseHeading']",
+                entityChartCollapse: "[data-id='entityChartCollapse']",
+                entityTypeSelection: ".entity-type-chart",
+                chartButton: ".chart-button"
+            },
+            /** ui events hash */
+            events: function() {
+                var events = {},
+                    that = this;
+                events["click " + this.ui.statisticsRefresh] = function(e) {
+                    this.showLoader();
+                    this.fetchMetricData();
+                    this.fetchStatusData();
+                    this.createMetricsChart();
+                };
+                events["change " + this.ui.metricsTimeList] = function(e) {
+                    var that = this,
+                        selectedTime = this.ui.metricsTimeList.val(),
+                        options = { isMetricsCollectionTime: true };
+                    if (selectedTime.length) {
+                        that.metricTime.url = UrlLinks.metricsCollectionTimeApiUrl() + selectedTime;
+                        that.showLoader();
+                        (selectedTime === "Current") ? that.fetchMetricData(): that.fetchMetricData(options);
+                        that.metricsChartDateRange = "7d";
+                        that.createMetricsChart();
+                        that.selectDefaultValueTimer();
+
+                        //that.ui.chartTimeLineList.val(that.metricsChartDateRange).trigger("change");
+                    }
+                };
+
+                // events["change " + this.ui.chartTimeLineList] = function(e) {
+                //     var that = this;
+                //     that.metricsChartDateRange = this.ui.chartTimeLineList.val();
+                //     that.createMetricsChart();
+                // };
+
+                events["click " + this.ui.chartButton] = function(e) {
+                    e.stopPropagation();
+                    that.$el.find('.chart-button').removeClass('active');
+                    $(e.currentTarget).addClass("active");
+                    that.metricsChartDateRange = $(e.currentTarget).children().text();
+                    that.createMetricsChart();
+
+                };
+                return events;
+            },
+            createMetricsChart: function() {
+                var that = this,
+                    selectedDateRange = that.dateRangesMap[that.metricsChartDateRange],
+                    startTime = Date.parse(that.getValue({ value: selectedDateRange[0], type: "day" })),
+                    endTime = Date.parse(that.getValue({ value: selectedDateRange[1], type: "day" }));
+                $(that.ui.chartTitle).text(that.entityType + " chart for " + that.dateRangeText[that.metricsChartDateRange]);
+                var options = {
+                    "startTime": startTime,
+                    "endTime": endTime,
+                    "typeName": that.entityType
+                };
+                that.showChartLoader();
+                that.createChartFromCollection(options);
+            },
+            /**
+             * intialize a new AboutAtlasView Layout
+             * @constructs
+             */
+            initialize: function(options) {
+                _.extend(this, options);
+                var that = this;
+                this.DATA_MAX_LENGTH = 25;
+                this.loaderCount = 0;
+                this.chartLoaderCount = 0;
+                this.metricCollectctionTime = new VTagList();
+                this.metricTime = new VTagList();
+                this.metricCollectctionTime.url = UrlLinks.metricsAllCollectionTimeApiUrl();
+                this.entityTypeChartCollection = new VTagList();
+                this.entityTypeChartCollection.url = UrlLinks.metricsGraphUrl();
+                this.entityType = "hive_table"; //default entity type for metrics chart
+                this.metricsChartDateRange = "7d"; //default range for metrics  chart 
+                this.freshLoad = true;
+                this.dateRangesMap = {
+                    'Today': moment(),
+                    '1d': [moment().subtract(1, 'days'), moment()],
+                    '7d': [moment().subtract(6, 'days'), moment()],
+                    '14d': [moment().subtract(13, 'days'), moment()],
+                    '30d': [moment().subtract(29, 'days'), moment()],
+                    'This Month': [moment().startOf('month'), moment().endOf('month')],
+                    'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
+                    'Last 3 Months': [moment().subtract(3, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
+                    'Last 6 Months': [moment().subtract(6, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
+                    'Last 12 Months': [moment().subtract(12, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
+                    'This Quarter': [moment().startOf('quarter'), moment().endOf('quarter')],
+                    'Last Quarter': [moment().subtract(1, 'quarter').startOf('quarter'), moment().subtract(1, 'quarter').endOf('quarter')],
+                    'This Year': [moment().startOf('year'), moment().endOf('year')],
+                    'Last Year': [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')]
+                };
+                this.dateRangeText = {
+                    '1d': "last 1 day",
+                    '7d': "last 7 days",
+                    '14d': "last 14 days",
+                    '30d': "last 30 days",
+                };
+                if (this.isMigrationView) {
+                    this.migrationImportStatus = new VTagList();
+                    this.migrationImportStatus.url = UrlLinks.migrationStatusApiUrl();
+                } else {
+                    var modal = new Modal({
+                        title: 'Statistics',
+                        content: this,
+                        okCloses: true,
+                        okText: "Close",
+                        showFooter: true,
+                        allowCancel: false,
+                        width: "60%",
+                        headerButtons: [{
+                            title: "Refresh Data",
+                            btnClass: "fa fa-refresh",
+                            onClick: function() {
+                                modal.$el.find('.header-button .fa-refresh').tooltip('hide').prop('disabled', true).addClass('fa-spin');
+                                // that.fetchMetricData({ update: true });
+                                that.freshChart = true;
+                                d3.selectAll("#compareGraph > svg > *").remove();
+                                that.fetchAllMetricsData({ update: true });
+                            }
+                        }]
+                    });
+                    modal.on('closeModal', function() {
+                        modal.trigger('cancel');
+                    });
+                    this.modal = modal;
+                    modal.open();
+                }
+            },
+            bindEvents: function() {
+                var that = this;
+                if (this.modal) {
+                    this.$el.on('click', '.linkClicked', function() {
+                        that.modal.close();
+                    })
+                }
+                this.$el.find("table").on('click', function(e) {
+                    var clickElement = $(e.target);
+                    if ($(this).hasClass("entityTable") && clickElement.parents().hasClass("entity-type-chart")) {
+                        $(this).find("tr").removeClass("table-row-selected");
+                        var parentRow = clickElement.parents("tr");
+                        parentRow.addClass("table-row-selected");
+                        if (that.entityType === parentRow.attr('id')) {
+                            that.ui.entityChartCollapse.toggleClass('in');
+                        } else {
+                            that.entityType = parentRow.attr('id');
+                            that.freshLoad = true;
+                            that.createMetricsChart();
+                            if (!that.ui.entityChartCollapse.hasClass('in')) {
+                                that.ui.entityChartCollapse.toggleClass('in');
+                            }
+                        }
+                    }
+                });
+            },
+            onRender: function() {
+                this.bindEvents();
+                this.createChartTimeLineDropdown(); //create dropdown for chart with days options
+                this.fetchAllMetricsData();
+                this.createTimeLineDropdown();
+            },
+            fetchStatusData: function() {
+                var that = this;
+                ++this.loaderCount;
+                that.migrationImportStatus.fetch({
+                    success: function(data) {
+                        var data = _.first(data.toJSON()),
+                            migrationStatus = data.MigrationStatus || null,
+                            operationStatus = migrationStatus.operationStatus,
+                            showProgress = true,
+                            totalProgress = 0,
+                            progressMessage = "";
+                        if (migrationStatus) {
+                            if (MigrationEnums.migrationStatus[operationStatus] === "DONE") {
+                                showProgress = false;
+                            } else if (MigrationEnums.migrationStatus[operationStatus] === "IN_PROGRESS" || MigrationEnums.migrationStatus[operationStatus] === "STARTED") {
+                                var currentIndex = migrationStatus.currentIndex || 0,
+                                    totalCount = migrationStatus.totalCount || 0;
+                                totalProgress = Math.ceil((migrationStatus.currentIndex / migrationStatus.totalCount) * 100)
+                                progressMessage = totalProgress + "%";
+                                that.ui.migrationProgressBar.removeClass("progress-bar-danger");
+                                that.ui.migrationProgressBar.addClass("progress-bar-success");
+                            } else if (MigrationEnums.migrationStatus[operationStatus] === "FAIL") {
+                                totalProgress = "100";
+                                progressMessage = "Failed";
+                                that.ui.migrationProgressBar.addClass("progress-bar-danger");
+                                that.ui.migrationProgressBar.removeClass("progress-bar-success");
+                            }
+                            if (showProgress) {
+                                that.$el.find(".statistics-header>.progress").removeClass("hide");
+                                that.$el.find(".statistics-header>.successStatus").addClass("hide");
+                                that.ui.migrationProgressBar.css({ width: totalProgress + '%' });
+                                that.ui.migrationProgressBarValue.text(progressMessage);
+                            } else {
+                                that.$el.find(".statistics-header>.progress").addClass("hide");
+                                that.$el.find(".statistics-header>.successStatus").removeClass("hide");
+                            }
+                        }
+
+                    },
+                    complete: function() {
+                        --that.loaderCount;
+                        that.hideLoader();
+                    }
+                });
+            },
+            fetchAllMetricsData: function(options) {
+                this.fetchMetricCollectionTime(); // create dropdown as per collectionTime
+                this.showLoader();
+                if (this.isMigrationView) {
+                    this.fetchStatusData();
+                }
+                this.fetchMetricData(options); // display all the required metrics data
+                this.createMetricsChart(); // display Metrics in chart
+                this.selectDefaultValueTimer();
+            },
+            selectDefaultValueTimer: function(options) {
+                var that = this;
+                this.defaultValues = setTimeout(function() {
+                    that.$el.find('#' + that.entityType).addClass('table-row-selected');
+                    that.resetChartButtonSelection();
+                    that.selectDefaultValueTimerStop();
+                }, 1200);
+            },
+            selectDefaultValueTimerStop: function() {
+                clearTimeout(this.defaultValues);
+            },
+            resetChartButtonSelection: function() {
+                var that = this;
+                this.$el.find('.chart-button').removeClass('active');
+                _.each(this.$el.find('.chart-button'), function(chartButton) {
+                    if (chartButton.innerText === that.metricsChartDateRange) {
+                        $(chartButton).addClass("active")
+                    }
+                });
+            },
+            fetchMetricCollectionTime: function() {
+                var that = this;
+                ++this.loaderCount;
+                this.showLoader();
+                this.metricCollectctionTime.fetch({
+                    success: function(data) {
+                        var data = data ? data.toJSON() : null;
+                        data.length ? that.createTimeLineDropdown(data) : that.createTimeLineDropdown();
+                    },
+                    complete: function() {
+                        --that.loaderCount;
+                        that.hideLoader()
+                    }
+                });
+            },
+            fetchMetricData: function(options) {
+                var that = this,
+                    collectionTofetch = (options && options.isMetricsCollectionTime) ? that.metricTime : that.metricCollection;
+                ++this.loaderCount;
+
+                collectionTofetch.fetch({
+                    success: function(data) {
+                        var data = _.first(data.toJSON());
+                        // data = (options && options.isMetricsCollectionTime) ? data.data : data;
+                        if (options && options.isMetricsCollectionTime) {
+                            data = (data.metrics && data.metrics.data) ? data.metrics.data : data;
+                        }
+                        that.renderStats({ valueObject: data.general.stats, dataObject: data.general });
+                        that.renderEntities({ data: data });
+                        that.renderSystemDeatils({ data: data });
+                        that.renderClassifications({ data: data });
+                        if (options && options.update) {
+                            if (that.modal) {
+                                that.modal.$el.find('.header-button .fa-refresh').prop('disabled', false).removeClass('fa-spin');
+                            }
+                            Utils.notifySuccess({
+                                content: "Metric data is refreshed"
+                            })
+                        }
+                    },
+                    complete: function() {
+                        --that.loaderCount;
+                        that.hideLoader();
+                    }
+                });
+            },
+            createChartFromCollection: function(params) {
+                var that = this,
+                    chartData = {};
+                ++that.chartLoaderCount;
+                console.log("startTime: " + new Date(params.startTime) + ", endTime: " + new Date(params.endTime) + " ,typeName: " + params.typeName);
+                that.entityTypeChartCollection.queryParams = params;
+                that.entityTypeChartCollection.fetch({
+                    success: function(data) {
+                        chartData = _.first(data.toJSON());
+                        that.renderStackAreaGraps(chartData[that.entityType]);
+                        if (that.freshLoad) {
+                            that.freshLoad = false;
+                            that.selectDefaultChartTimer(chartData[that.entityType]);
+                        }
+                    },
+                    complete: function() {
+                        --that.chartLoaderCount;
+                        that.hideChartLoader();
+                    }
+                });
+            },
+            selectDefaultChartTimer: function(chartData) {
+                var that = this;
+                this.chartRenderTimer = setTimeout(function() {
+                    that.renderStackAreaGraps(chartData);
+                    that.selectDefaultChartTimerStop();
+                }, 1500);
+            },
+            selectDefaultChartTimerStop: function() {
+                clearTimeout(this.chartRenderTimer);
+            },
+            createTimeLineDropdown: function(data) {
+                var that = this,
+                    options = '<option value="Current" data-name="Current" selected>Current</option>';
+                that.ui.metricsTimeList.empty();
+                _.each(data, function(data) {
+                    var collectionTime = that.getValue({ type: "day", value: data.collectionTime });
+                    options += '<option value="' + (data.collectionTime) + '" data-name="' + (data.collectionTime) + '">' + collectionTime + '</option>';
+                });
+                that.ui.metricsTimeList.html(options);
+            },
+            createChartTimeLineDropdown: function() {
+                var that = this,
+                    options = '<ul class="pull-right">';
+                options += '<li class="chart-button"><a href="javascript:void(0);"  title="Last 1 day"  data-day="1d">1d</a></li>';
+                options += '<li class="chart-button active"><a href="javascript:void(0);"  title="Last 7 days"  data-day="7d">7d</a></li>';
+                options += '<li class="chart-button"><a href="javascript:void(0);"  title="Last 14 days"  data-day="14d">14d</a></li>';
+                options += '<li class="chart-button"><a href="javascript:void(0);"  title="Last 30 days"  data-day="30d">30d</a></li>';
+                options += '</ul>'
+                that.ui.chartTimeLineList.empty();
+                that.ui.chartTimeLineList.html(options);
+            },
+            createTimeLineDropdownGraph: function() {
+                this.defaultRange = "7d";
+                this.dateValue = null;
+                var that = this,
+                    isTimeRange = true,
+                    obj = {
+                        opens: "center",
+                        autoApply: true,
+                        autoUpdateInput: false,
+                        timePickerSeconds: true,
+                        timePicker: true,
+                        locale: {
+                            format: Globals.dateTimeFormat
+                        }
+                    };
+                if (isTimeRange) {
+                    var defaultRangeDate = this.dateRangesMap[this.defaultRange];
+                    obj.startDate = defaultRangeDate[0];
+                    obj.endDate = defaultRangeDate[1];
+                    obj.singleDatePicker = false;
+                    obj.ranges = this.dateRangesMap;
+                } else {
+                    obj.singleDatePicker = true;
+                    obj.startDate = moment();
+                    obj.endDate = obj.startDate;
+                }
+                var inputEl = this.ui.metricsTimeLine;
+
+                inputEl.attr('readonly', true);
+                inputEl.daterangepicker(obj);
+                inputEl.on('apply.daterangepicker', function(ev, picker) {
+                    picker.setStartDate(picker.startDate);
+                    picker.setEndDate(picker.endDate);
+                    var valueString = "";
+                    if (picker.chosenLabel) {
+                        if (picker.chosenLabel === "Custom Range") {
+                            valueString = picker.startDate.format(Globals.dateTimeFormat) + " - " + picker.endDate.format(Globals.dateTimeFormat);
+                        } else {
+                            valueString = picker.chosenLabel;
+                        }
+                    } else {
+                        valueString = picker.startDate.format(Globals.dateTimeFormat);
+                    }
+                    picker.element.val(valueString);
+                    that.dateValue = valueString;
+                });
+            },
+            hideLoader: function() {
+                if (this.loaderCount === 0) {
+                    var className = ".statsContainer";
+                    if (this.isMigrationView) {
+                        className += ",.statistics-header";
+                    }
+                    this.$(className).removeClass('hide');
+                    this.$('.statsLoader').removeClass('show');
+                }
+            },
+            showLoader: function() {
+                var className = ".statsContainer";
+                if (this.isMigrationView) {
+                    className += ",.statistics-header";
+                }
+                this.$(className).addClass('hide');
+                this.$('.statsLoader').addClass('show');
+            },
+            hideChartLoader: function() {
+                if (this.chartLoaderCount === 0) {
+                    // var className = ".chart-container";
+                    // this.$(className).removeClass('hide');
+                    this.$('.statsLoader').removeClass('show');
+                    this.$('.modalOverlay').addClass('hide');
+                }
+            },
+            showChartLoader: function() {
+                // var className = ".chart-container";
+                // this.$(className).addClass('hide');
+                this.$('.statsLoader').addClass('show');
+                this.$('.modalOverlay').removeClass('hide');
+            },
+            closePanel: function(options) {
+                var el = options.el;
+                el.find(">.panel-heading").attr("aria-expanded", "false");
+                el.find(">.panel-collapse.collapse").removeClass("in");
+            },
+            genrateStatusData: function(stateObject) {
+                var that = this,
+                    stats = {};
+                _.each(stateObject, function(val, key) {
+                    var keys = key.split(":"),
+                        key = keys[0],
+                        subKey = keys[1];
+                    if (stats[key]) {
+                        stats[key][subKey] = val;
+                    } else {
+                        stats[key] = {};
+                        stats[key][subKey] = val;
+                    }
+                });
+                return stats;
+            },
+            createTable: function(obj) {
+                var that = this,
+                    tableBody = '',
+                    type = obj.type,
+                    data = obj.data;
+                _.each(data, function(value, key, list) {
+                    var newValue = that.getValue({
+                        "value": value
+                    });
+                    if (type === "classification") {
+                        newValue = '<a title="Search for entities associated with \'' + key + '\'" class="linkClicked" href="#!/search/searchResult?searchType=basic&tag=' + key + '">' + newValue + '<a>';
+                    }
+                    tableBody += '<tr><td>' + key + '</td><td class="">' + newValue + '</td></tr>';
+                });
+                return tableBody;
+            },
+            renderClassifications: function(options) {
+                var that = this,
+                    data = options.data,
+                    classificationData = data.tag || {},
+                    tagEntitiesData = classificationData ? classificationData.tagEntities || {} : {},
+                    tagsCount = 0,
+                    newTagEntitiesData = {},
+                    tagEntitiesKeys = _.keys(tagEntitiesData);
+                _.each(_.sortBy(tagEntitiesKeys, function(o) {
+                    return o.toLocaleLowerCase();
+                }), function(key) {
+                    var val = tagEntitiesData[key];
+                    newTagEntitiesData[key] = val;
+                    tagsCount += val;
+                });
+                tagEntitiesData = newTagEntitiesData;
+
+                if (!_.isEmpty(tagEntitiesData)) {
+                    this.ui.classificationCard.html(
+                        that.createTable({
+                            "data": tagEntitiesData,
+                            "type": "classification"
+                        })
+                    );
+                    this.ui.classification.find(".count").html("&nbsp;(" + _.numberFormatWithComma(tagsCount) + ")");
+                    if (tagEntitiesKeys.length > this.DATA_MAX_LENGTH) {
+                        this.closePanel({
+                            el: this.ui.classification
+                        })
+                    }
+                }
+            },
+            renderEntities: function(options) {
+                var that = this,
+                    data = options.data,
+                    entityData = data.entity,
+                    activeEntities = entityData.entityActive || {},
+                    deletedEntities = entityData.entityDeleted || {},
+                    shellEntities = entityData.entityShell || {},
+                    stats = {},
+                    activeEntityCount = 0,
+                    deletedEntityCount = 0,
+                    shellEntityCount = 0,
+                    createEntityData = function(opt) {
+                        var entityData = opt.entityData,
+                            type = opt.type;
+                        _.each(entityData, function(val, key) {
+                            var intVal = _.isUndefined(val) ? 0 : val;
+                            if (type == "active") {
+                                activeEntityCount += intVal;
+                            }
+                            if (type == "deleted") {
+                                deletedEntityCount += intVal;
+                            }
+                            if (type == "shell") {
+                                shellEntityCount += intVal
+                            }
+                            intVal = _.numberFormatWithComma(intVal)
+                            if (stats[key]) {
+                                stats[key][type] = intVal;
+                            } else {
+                                stats[key] = {};
+                                stats[key][type] = intVal;
+                            }
+                        })
+                    };
+
+                createEntityData({
+                    "entityData": activeEntities,
+                    "type": "active"
+                })
+                createEntityData({
+                    "entityData": deletedEntities,
+                    "type": "deleted"
+                });
+                createEntityData({
+                    "entityData": shellEntities,
+                    "type": "shell"
+                });
+                if (!_.isEmpty(stats)) {
+                    var statsKeys = _.keys(stats);
+                    this.ui.entityCard.html(
+                        EntityTable({
+                            "data": _.pick(stats, _.sortBy(statsKeys, function(o) {
+                                return o.toLocaleLowerCase();
+                            })),
+                        })
+                    );
+                    this.$('[data-id="activeEntity"]').html("&nbsp;(" + _.numberFormatWithComma(activeEntityCount) + ")");
+                    this.$('[data-id="deletedEntity"]').html("&nbsp;(" + _.numberFormatWithComma(deletedEntityCount) + ")");
+                    this.$('[data-id="shellEntity"]').html("&nbsp;(" + _.numberFormatWithComma(shellEntityCount) + ")");
+                    this.ui.entity.find(".count").html("&nbsp;(" + _.numberFormatWithComma(data.general.entityCount) + ")");
+                    if (statsKeys.length > this.DATA_MAX_LENGTH) {
+                        this.closePanel({
+                            el: this.ui.entity
+                        })
+                    }
+                }
+            },
+            renderStats: function(options) {
+                var that = this,
+                    data = this.genrateStatusData(options.valueObject),
+                    generalData = options.dataObject,
+                    createTable = function(obj) {
+                        var tableBody = '',
+                            enums = obj.enums,
+                            data = obj.data;
+                        _.each(data, function(value, key, list) {
+                            tableBody += '<tr><td>' + key + '</td><td class="">' + that.getValue({
+                                "value": value,
+                                "type": enums[key]
+                            }) + '</td></tr>';
+                        });
+                        return tableBody;
+                    };
+                if (!that.isMigrationView && data.Notification) {
+                    var tableCol = [{
+                                label: "Total <br> (from " + (that.getValue({
+                                    "value": data.Server["startTimeStamp"],
+                                    "type": Enums.stats.Server["startTimeStamp"],
+                                })) + ")",
+                                key: "total"
+                            },
+                            {
+                                label: "Current Hour <br> (from " + (that.getValue({
+                                    "value": data.Notification["currentHourStartTime"],
+                                    "type": Enums.stats.Notification["currentHourStartTime"],
+                                })) + ")",
+                                key: "currentHour"
+                            },
+                            { label: "Previous Hour", key: "previousHour" },
+                            {
+                                label: "Current Day <br> (from " + (that.getValue({
+                                    "value": data.Notification["currentDayStartTime"],
+                                    "type": Enums.stats.Notification["currentDayStartTime"],
+                                })) + ")",
+                                key: "currentDay"
+                            },
+                            { label: "Previous Day", key: "previousDay" }
+                        ],
+                        tableHeader = ["count", "AvgTime", "EntityCreates", "EntityUpdates", "EntityDeletes", "Failed"];
+                    that.ui.notificationCard.html(
+                        StatsNotiTable({
+                            "enums": Enums.stats.Notification,
+                            "data": data.Notification,
+                            "tableHeader": tableHeader,
+                            "tableCol": tableCol,
+                            "getTmplValue": function(argument, args) {
+                                var pickValueFrom = argument.key.concat(args);
+                                if (argument.key == "total" && args == "EntityCreates") {
+                                    pickValueFrom = "totalCreates";
+                                } else if (argument.key == "total" && args == "EntityUpdates") {
+                                    pickValueFrom = "totalUpdates";
+                                } else if (argument.key == "total" && args == "EntityDeletes") {
+                                    pickValueFrom = "totalDeletes";
+                                } else if (args == "count") {
+                                    pickValueFrom = argument.key;
+                                }
+                                var returnVal = data.Notification[pickValueFrom];
+                                return returnVal ? _.numberFormatWithComma(returnVal) : 0;
+                            }
+                        })
+                    );
+
+                    var offsetTableColumn = function(obj) {
+                        var returnObj = []
+                        _.each(obj, function(value, key) {
+                            returnObj.push({ "label": key, "dataValue": value });
+                        });
+                        return returnObj
+                    }
+
+                    that.ui.offsetCard.html(
+                        TopicOffsetTable({
+                            data: data.Notification.topicDetails,
+                            tableHeader: ["offsetStart", "offsetCurrent", "processedMessageCount", "failedMessageCount", "lastMessageProcessedTime"],
+                            tableCol: offsetTableColumn(data.Notification.topicDetails),
+                            getTmplValue: function(argument, args) {
+                                var returnVal = data.Notification.topicDetails[argument.label][args];
+                                return returnVal ? that.getValue({ value: returnVal, type: Enums.stats.Notification[args] }) : 0;
+                            }
+                        })
+                    )
+                    that.ui.notificationDetails.removeClass('hide');
+                }
+
+                if (data.Server) {
+                    that.ui.serverCard.html(
+                        createTable({
+                            "enums": _.extend(Enums.stats.Server, Enums.stats.ConnectionStatus, Enums.stats.generalData),
+                            "data": _.extend(
+                                _.pick(data.Server, 'startTimeStamp', 'activeTimeStamp', 'upTime', 'statusBackendStore', 'statusIndexStore'),
+                                _.pick(generalData, 'collectionTime'))
+                        })
+                    );
+                }
+            },
+            renderSystemDeatils: function(options) {
+                var that = this,
+                    data = options.data,
+                    systemData = data.system,
+                    systemOS = systemData.os || {},
+                    systemRuntimeData = systemData.runtime || {},
+                    systemMemoryData = systemData.memory || {};
+                if (!_.isEmpty(systemOS)) {
+                    that.ui.osCard.html(
+                        that.createTable({
+                            "data": systemOS
+                        })
+                    );
+                }
+                if (!_.isEmpty(systemRuntimeData)) {
+                    that.ui.runtimeCard.html(
+                        that.createTable({
+                            "data": systemRuntimeData
+                        })
+                    );
+                }
+                if (!_.isEmpty(systemMemoryData)) {
+                    var memoryTable = CommonViewFunction.propertyTable({
+                        scope: this,
+                        formatStringVal: true,
+                        valueObject: systemMemoryData,
+                        numberFormat: _.numberFormatWithBytes
+                    });
+                    that.ui.memoryCard.html(
+                        memoryTable);
+                }
+            },
+            getValue: function(options) {
+                var value = options.value,
+                    type = options.type;
+                if (type == 'time') {
+                    return Utils.millisecondsToTime(value);
+                } else if (type == 'day') {
+                    return Utils.formatDate({ date: value })
+                } else if (type == 'number') {
+                    return _.numberFormatWithComma(value);
+                } else if (type == 'millisecond') {
+                    return _.numberFormatWithComma(value) + " millisecond/s";
+                } else if (type == "status-html") {
+                    return '<span class="connection-status ' + value + '"></span>';
+                } else {
+                    return value;
+                }
+            },
+            renderStackAreaGraps: function(data) {
+                var that = this,
+                    data = data;
+
+                nv.addGraph(function() {
+                    var chart = nv.models.stackedAreaChart()
+                        .margin({ right: 60 })
+                        .x(function(d) { return d[0] }) //We can modify the data accessor functions...
+                        .y(function(d) { return d[1] }) //...in case your data is formatted differently.
+                        .useInteractiveGuideline(true) //Tooltips which show all data points. Very nice!
+                        .rightAlignYAxis(false) //Let's move the y-axis to the right side.
+                        //.transitionDuration(500)
+                        .showControls(true) //Allow user to choose 'Stacked', 'Stream', 'Expanded' mode.
+                        .clipEdge(true);
+
+                    //Format x-axis labels with custom function.
+                    chart.xAxis
+                        .tickFormat(function(d) {
+                            return d3.time.format('%x')(new Date(d));
+                        });
+
+                    chart.yAxis
+                        .tickFormat(d3.format(',.2f'));
+
+                    var graphTimer = setTimeout(function() {
+                        d3.select(that.ui.compareGraph[0])
+                            .datum(data).transition().duration(500)
+                            .call(chart);
+                        clearTimeout(graphTimer);
+                    }, 300);
+
+                    d3.select(window).on('mouseout', function() {
+                        d3.selectAll('.nvtooltip').style('opacity', '0');
+                    });
+
+                    nv.utils.windowResize(chart.update);
+
+                    return chart;
+                });
+            }
+        });
+    return StatisticsTimelineView;
+});
\ No newline at end of file
diff --git a/dashboardv2/public/js/views/site/d3.v3.min.js b/dashboardv2/public/js/views/site/d3.v3.min.js
new file mode 100644
index 0000000..3d0fa3b
--- /dev/null
+++ b/dashboardv2/public/js/views/site/d3.v3.min.js
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(argum [...]
+r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)<Uo||xo(r [...]
+return p}:En(r),w=i===u?function(){return g}:En(u);++m<M;)o.call(this,h=t[m],m)?(d.push([p=+x.call(this,h,m),g=+b.call(this,h,m)]),y.push([+_.call(this,h,m),+w.call(this,h,m)])):d.length&&(l(),d=[],y=[]);return d.length&&l(),v.length?v.join(""):null}var e=Ce,r=Ce,i=0,u=ze,o=zt,a=xu,l=a.key,c=a,f="L",s=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return [...]
+shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.g [...]
+if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++u<o;)i=n[u],i.x=a,i.y=c,i.dy=f,a+=i.dx=Math.min(e.x+e.dx-a,f?l(i.area/f):0);i.z=!0,i.dx+=e.x+e.dx-a,e.y+=f,e.dy-=f}else{for((r||f>e.dx)&&(f=e.dx);++u<o;)i=n[u],i.x=a,i.y=c,i.dx=f,c+=i.dy=Math.min(e.y+e.dy-c,f?l(i.area/f):0);i.z=!1,i.dy+=e.y+e.dy-c,e.x+=f,e.dx-=f}}function u(r){var i=o||a(r),u=i[0];return u.x=u.y=0,u.value?(u.dx=c[0],u.dy=c[1]):u.dx=u.dy=0,o&&a.revalue(u),n([u],u.dx*u.dy/u.value),(o?e:t)(u),h&&(o=i),i}var o,a=ao.layout.hierarchy(),l [...]
\ No newline at end of file
diff --git a/dashboardv2/public/js/views/site/nv.d3.js b/dashboardv2/public/js/views/site/nv.d3.js
new file mode 100644
index 0000000..2e65a7c
--- /dev/null
+++ b/dashboardv2/public/js/views/site/nv.d3.js
@@ -0,0 +1,18144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ 
+/* nvd3 version 1.8.6-dev (https://github.com/novus/nvd3) 2018-02-24 */
+(function(){
+
+// set up main nv object
+var nv = {};
+
+// the major global objects under the nv namespace
+nv.dev = false; //set false when in production
+nv.tooltip = nv.tooltip || {}; // For the tooltip system
+nv.utils = nv.utils || {}; // Utility subsystem
+nv.models = nv.models || {}; //stores all the possible models/components
+nv.charts = {}; //stores all the ready to use charts
+nv.logs = {}; //stores some statistics and potential error messages
+nv.dom = {}; //DOM manipulation functions
+
+// Node/CommonJS - require D3
+if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined' && typeof(d3) == 'undefined') {
+    d3 = require('d3');
+}
+
+nv.dispatch = d3.dispatch('render_start', 'render_end');
+
+// Function bind polyfill
+// Needed ONLY for phantomJS as it's missing until version 2.0 which is unreleased as of this comment
+// https://github.com/ariya/phantomjs/issues/10522
+// http://kangax.github.io/compat-table/es5/#Function.prototype.bind
+// phantomJS is used for running the test suite
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function (oThis) {
+        if (typeof this !== "function") {
+            // closest thing possible to the ECMAScript 5 internal IsCallable function
+            throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
+        }
+
+        var aArgs = Array.prototype.slice.call(arguments, 1),
+            fToBind = this,
+            fNOP = function () {},
+            fBound = function () {
+                return fToBind.apply(this instanceof fNOP && oThis
+                        ? this
+                        : oThis,
+                    aArgs.concat(Array.prototype.slice.call(arguments)));
+            };
+
+        fNOP.prototype = this.prototype;
+        fBound.prototype = new fNOP();
+        return fBound;
+    };
+}
+
+//  Development render timers - disabled if dev = false
+if (nv.dev) {
+    nv.dispatch.on('render_start', function(e) {
+        nv.logs.startTime = +new Date();
+    });
+
+    nv.dispatch.on('render_end', function(e) {
+        nv.logs.endTime = +new Date();
+        nv.logs.totalTime = nv.logs.endTime - nv.logs.startTime;
+        nv.log('total', nv.logs.totalTime); // used for development, to keep track of graph generation times
+    });
+}
+
+// Logs all arguments, and returns the last so you can test things in place
+// Note: in IE8 console.log is an object not a function, and if modernizr is used
+// then calling Function.prototype.bind with with anything other than a function
+// causes a TypeError to be thrown.
+nv.log = function() {
+    if (nv.dev && window.console && console.log && console.log.apply)
+        console.log.apply(console, arguments);
+    else if (nv.dev && window.console && typeof console.log == "function" && Function.prototype.bind) {
+        var log = Function.prototype.bind.call(console.log, console);
+        log.apply(console, arguments);
+    }
+    return arguments[arguments.length - 1];
+};
+
+// print console warning, should be used by deprecated functions
+nv.deprecated = function(name, info) {
+    if (console && console.warn) {
+        console.warn('nvd3 warning: `' + name + '` has been deprecated. ', info || '');
+    }
+};
+
+// The nv.render function is used to queue up chart rendering
+// in non-blocking async functions.
+// When all queued charts are done rendering, nv.dispatch.render_end is invoked.
+nv.render = function render(step) {
+    // number of graphs to generate in each timeout loop
+    step = step || 1;
+
+    nv.render.active = true;
+    nv.dispatch.render_start();
+
+    var renderLoop = function() {
+        var chart, graph;
+
+        for (var i = 0; i < step && (graph = nv.render.queue[i]); i++) {
+            chart = graph.generate();
+            if (typeof graph.callback == typeof(Function)) graph.callback(chart);
+        }
+
+        nv.render.queue.splice(0, i);
+
+        if (nv.render.queue.length) {
+            setTimeout(renderLoop);
+        }
+        else {
+            nv.dispatch.render_end();
+            nv.render.active = false;
+        }
+    };
+
+    setTimeout(renderLoop);
+};
+
+nv.render.active = false;
+nv.render.queue = [];
+
+/*
+Adds a chart to the async rendering queue. This method can take arguments in two forms:
+nv.addGraph({
+    generate: <Function>
+    callback: <Function>
+})
+
+or
+
+nv.addGraph(<generate Function>, <callback Function>)
+
+The generate function should contain code that creates the NVD3 model, sets options
+on it, adds data to an SVG element, and invokes the chart model. The generate function
+should return the chart model.  See examples/lineChart.html for a usage example.
+
+The callback function is optional, and it is called when the generate function completes.
+*/
+nv.addGraph = function(obj) {
+    if (typeof arguments[0] === typeof(Function)) {
+        obj = {generate: arguments[0], callback: arguments[1]};
+    }
+
+    nv.render.queue.push(obj);
+
+    if (!nv.render.active) {
+        nv.render();
+    }
+};
+
+// Node/CommonJS exports
+if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') {
+  module.exports = nv;
+}
+
+if (typeof(window) !== 'undefined') {
+  window.nv = nv;
+}
+/* Facade for queueing DOM write operations
+ * with Fastdom (https://github.com/wilsonpage/fastdom)
+ * if available.
+ * This could easily be extended to support alternate
+ * implementations in the future.
+ */
+nv.dom.write = function(callback) {
+    if (window.fastdom !== undefined) {
+        return fastdom.mutate(callback);
+    }
+    return callback();
+};
+
+/* Facade for queueing DOM read operations
+ * with Fastdom (https://github.com/wilsonpage/fastdom)
+ * if available.
+ * This could easily be extended to support alternate
+ * implementations in the future.
+ */
+nv.dom.read = function(callback) {
+    if (window.fastdom !== undefined) {
+        return fastdom.measure(callback);
+    }
+    return callback();
+};
+/* Utility class to handle creation of an interactive layer.
+ This places a rectangle on top of the chart. When you mouse move over it, it sends a dispatch
+ containing the X-coordinate. It can also render a vertical line where the mouse is located.
+
+ dispatch.elementMousemove is the important event to latch onto.  It is fired whenever the mouse moves over
+ the rectangle. The dispatch is given one object which contains the mouseX/Y location.
+ It also has 'pointXValue', which is the conversion of mouseX to the x-axis scale.
+ */
+nv.interactiveGuideline = function() {
+    "use strict";
+
+    var margin = { left: 0, top: 0 } //Pass the chart's top and left magins. Used to calculate the mouseX/Y.
+        ,   width = null
+        ,   height = null
+        ,   xScale = d3.scale.linear()
+        ,   dispatch = d3.dispatch('elementMousemove', 'elementMouseout', 'elementClick', 'elementDblclick', 'elementMouseDown', 'elementMouseUp')
+        ,   showGuideLine = true
+        ,   svgContainer = null // Must pass the chart's svg, we'll use its mousemove event.
+        ,   tooltip = nv.models.tooltip()
+        ,   isMSIE =  window.ActiveXObject// Checkt if IE by looking for activeX. (excludes IE11)
+    ;
+
+    tooltip
+        .duration(0)
+        .hideDelay(0)
+        .hidden(false);
+
+    function layer(selection) {
+        selection.each(function(data) {
+            var container = d3.select(this);
+            var availableWidth = (width || 960), availableHeight = (height || 400);
+            var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer")
+                .data([data]);
+            var wrapEnter = wrap.enter()
+                .append("g").attr("class", " nv-wrap nv-interactiveLineLayer");
+            wrapEnter.append("g").attr("class","nv-interactiveGuideLine");
+
+            if (!svgContainer) {
+                return;
+            }
+
+            function mouseHandler() {
+                var mouseX = d3.event.clientX - this.getBoundingClientRect().left;
+                var mouseY = d3.event.clientY - this.getBoundingClientRect().top;
+
+                var subtractMargin = true;
+                var mouseOutAnyReason = false;
+                if (isMSIE) {
+                    /*
+                     D3.js (or maybe SVG.getScreenCTM) has a nasty bug in Internet Explorer 10.
+                     d3.mouse() returns incorrect X,Y mouse coordinates when mouse moving
+                     over a rect in IE 10.
+                     However, d3.event.offsetX/Y also returns the mouse coordinates
+                     relative to the triggering <rect>. So we use offsetX/Y on IE.
+                     */
+                    mouseX = d3.event.offsetX;
+                    mouseY = d3.event.offsetY;
+
+                    /*
+                     On IE, if you attach a mouse event listener to the <svg> container,
+                     it will actually trigger it for all the child elements (like <path>, <circle>, etc).
+                     When this happens on IE, the offsetX/Y is set to where ever the child element
+                     is located.
+                     As a result, we do NOT need to subtract margins to figure out the mouse X/Y
+                     position under this scenario. Removing the line below *will* cause
+                     the interactive layer to not work right on IE.
+                     */
+                    if(d3.event.target.tagName !== "svg") {
+                        subtractMargin = false;
+                    }
+
+                    if (d3.event.target.className.baseVal.match("nv-legend")) {
+                        mouseOutAnyReason = true;
+                    }
+
+                }
+
+                if(subtractMargin) {
+                    mouseX -= margin.left;
+                    mouseY -= margin.top;
+                }
+
+                /* If mouseX/Y is outside of the chart's bounds,
+                 trigger a mouseOut event.
+                 */
+                if (d3.event.type === 'mouseout'
+                    || mouseX < 0 || mouseY < 0
+                    || mouseX > availableWidth || mouseY > availableHeight
+                    || (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined)
+                    || mouseOutAnyReason
+                    ) {
+
+                    if (isMSIE) {
+                        if (d3.event.relatedTarget
+                            && d3.event.relatedTarget.ownerSVGElement === undefined
+                            && (d3.event.relatedTarget.className === undefined
+                                || d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass))) {
+
+                            return;
+                        }
+                    }
+                    dispatch.elementMouseout({
+                        mouseX: mouseX,
+                        mouseY: mouseY
+                    });
+                    layer.renderGuideLine(null); //hide the guideline
+                    tooltip.hidden(true);
+                    return;
+                } else {
+                    tooltip.hidden(false);
+                }
+
+
+                var scaleIsOrdinal = typeof xScale.rangeBands === 'function';
+                var pointXValue = undefined;
+
+                // Ordinal scale has no invert method
+                if (scaleIsOrdinal) {
+                    var elementIndex = d3.bisect(xScale.range(), mouseX) - 1;
+                    // Check if mouseX is in the range band
+                    if (xScale.range()[elementIndex] + xScale.rangeBand() >= mouseX) {
+                        pointXValue = xScale.domain()[d3.bisect(xScale.range(), mouseX) - 1];
+                    }
+                    else {
+                        dispatch.elementMouseout({
+                            mouseX: mouseX,
+                            mouseY: mouseY
+                        });
+                        layer.renderGuideLine(null); //hide the guideline
+                        tooltip.hidden(true);
+                        return;
+                    }
+                }
+                else {
+                    pointXValue = xScale.invert(mouseX);
+                }
+
+                dispatch.elementMousemove({
+                    mouseX: mouseX,
+                    mouseY: mouseY,
+                    pointXValue: pointXValue
+                });
+
+                //If user double clicks the layer, fire a elementDblclick
+                if (d3.event.type === "dblclick") {
+                    dispatch.elementDblclick({
+                        mouseX: mouseX,
+                        mouseY: mouseY,
+                        pointXValue: pointXValue
+                    });
+                }
+
+                // if user single clicks the layer, fire elementClick
+                if (d3.event.type === 'click') {
+                    dispatch.elementClick({
+                        mouseX: mouseX,
+                        mouseY: mouseY,
+                        pointXValue: pointXValue
+                    });
+                }
+
+                // if user presses mouse down the layer, fire elementMouseDown
+                if (d3.event.type === 'mousedown') {
+                    dispatch.elementMouseDown({
+                        mouseX: mouseX,
+                        mouseY: mouseY,
+                        pointXValue: pointXValue
+                    });
+                }
+
+                // if user presses mouse down the layer, fire elementMouseUp
+                if (d3.event.type === 'mouseup') {
+                    dispatch.elementMouseUp({
+                        mouseX: mouseX,
+                        mouseY: mouseY,
+                        pointXValue: pointXValue
+                    });
+                }
+            }
+
+            svgContainer
+                .on("touchmove",mouseHandler)
+                .on("mousemove",mouseHandler, true)
+                .on("mouseout" ,mouseHandler,true)
+                .on("mousedown" ,mouseHandler,true)
+                .on("mouseup" ,mouseHandler,true)
+                .on("dblclick" ,mouseHandler)
+                .on("click", mouseHandler)
+            ;
+
+            layer.guideLine = null;
+            //Draws a vertical guideline at the given X postion.
+            layer.renderGuideLine = function(x) {
+                if (!showGuideLine) return;
+                if (layer.guideLine && layer.guideLine.attr("x1") === x) return;
+                nv.dom.write(function() {
+                    var line = wrap.select(".nv-interactiveGuideLine")
+                        .selectAll("line")
+                        .data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String);
+                    line.enter()
+                        .append("line")
+                        .attr("class", "nv-guideline")
+                        .attr("x1", function(d) { return d;})
+                        .attr("x2", function(d) { return d;})
+                        .attr("y1", availableHeight)
+                        .attr("y2",0);
+                    line.exit().remove();
+                });
+            }
+        });
+    }
+
+    layer.dispatch = dispatch;
+    layer.tooltip = tooltip;
+
+    layer.margin = function(_) {
+        if (!arguments.length) return margin;
+        margin.top    = typeof _.top    != 'undefined' ? _.top    : margin.top;
+        margin.left   = typeof _.left   != 'undefined' ? _.left   : margin.left;
+        return layer;
+    };
+
+    layer.width = function(_) {
+        if (!arguments.length) return width;
+        width = _;
+        return layer;
+    };
+
+    layer.height = function(_) {
+        if (!arguments.length) return height;
+        height = _;
+        return layer;
+    };
+
+    layer.xScale = function(_) {
+        if (!arguments.length) return xScale;
+        xScale = _;
+        return layer;
+    };
+
+    layer.showGuideLine = function(_) {
+        if (!arguments.length) return showGuideLine;
+        showGuideLine = _;
+        return layer;
+    };
+
+    layer.svgContainer = function(_) {
+        if (!arguments.length) return svgContainer;
+        svgContainer = _;
+        return layer;
+    };
+
+    return layer;
+};
+
+/* Utility class that uses d3.bisect to find the index in a given array, where a search value can be inserted.
+ This is different from normal bisectLeft; this function finds the nearest index to insert the search value.
+
+ For instance, lets say your array is [1,2,3,5,10,30], and you search for 28.
+ Normal d3.bisectLeft will return 4, because 28 is inserted after the number 10.  But interactiveBisect will return 5
+ because 28 is closer to 30 than 10.
+
+ Unit tests can be found in: interactiveBisectTest.html
+
+ Has the following known issues:
+ * Will not work if the data points move backwards (ie, 10,9,8,7, etc) or if the data points are in random order.
+ * Won't work if there are duplicate x coordinate values.
+ */
+nv.interactiveBisect = function (values, searchVal, xAccessor) {
+    "use strict";
+    if (! (values instanceof Array)) {
+        return null;
+    }
+    var _xAccessor;
+    if (typeof xAccessor !== 'function') {
+        _xAccessor = function(d) {
+            return d.x;
+        }
+    } else {
+        _xAccessor = xAccessor;
+    }
+    var _cmp = function(d, v) {
+        // Accessors are no longer passed the index of the element along with
+        // the element itself when invoked by d3.bisector.
+        //
+        // Starting at D3 v3.4.4, d3.bisector() started inspecting the
+        // function passed to determine if it should consider it an accessor
+        // or a comparator. This meant that accessors that take two arguments
+        // (expecting an index as the second parameter) are treated as
+        // comparators where the second argument is the search value against
+        // which the first argument is compared.
+        return _xAccessor(d) - v;
+    };
+
+    var bisect = d3.bisector(_cmp).left;
+    var index = d3.max([0, bisect(values,searchVal) - 1]);
+    var currentValue = _xAccessor(values[index]);
+
+    if (typeof currentValue === 'undefined') {
+        currentValue = index;
+    }
+
+    if (currentValue === searchVal) {
+        return index; //found exact match
+    }
+
+    var nextIndex = d3.min([index+1, values.length - 1]);
+    var nextValue = _xAccessor(values[nextIndex]);
+
+    if (typeof nextValue === 'undefined') {
+        nextValue = nextIndex;
+    }
+
+    if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal)) {
+        return index;
+    } else {
+        return nextIndex
+    }
+};
+
+/*
+ Returns the index in the array "values" that is closest to searchVal.
+ Only returns an index if searchVal is within some "threshold".
+ Otherwise, returns null.
+ */
+nv.nearestValueIndex = function (values, searchVal, threshold) {
+    "use strict";
+    var yDistMax = Infinity, indexToHighlight = null;
+    values.forEach(function(d,i) {
+        var delta = Math.abs(searchVal - d);
+        if ( d != null && delta <= yDistMax && delta < threshold) {
+            yDistMax = delta;
+            indexToHighlight = i;
+        }
+    });
+    return indexToHighlight;
+};
+
+/* Model which can be instantiated to handle tooltip rendering.
+ Example usage:
+ var tip = nv.models.tooltip().gravity('w').distance(23)
+ .data(myDataObject);
+
+ tip();    //just invoke the returned function to render tooltip.
+ */
+nv.models.tooltip = function() {
+    "use strict";
+
+    /*
+    Tooltip data. If data is given in the proper format, a consistent tooltip is generated.
+    Example Format of data:
+    {
+        key: "Date",
+        value: "August 2009",
+        series: [
+            {key: "Series 1", value: "Value 1", color: "#000"},
+            {key: "Series 2", value: "Value 2", color: "#00f"}
+        ]
+    }
+    */
+    var id = "nvtooltip-" + Math.floor(Math.random() * 100000) // Generates a unique id when you create a new tooltip() object.
+        ,   data = null
+        ,   gravity = 'w'   // Can be 'n','s','e','w'. Determines how tooltip is positioned.
+        ,   distance = 25 // Distance to offset tooltip from the mouse location.
+        ,   snapDistance = 0   // Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect)
+        ,   classes = null  // Attaches additional CSS classes to the tooltip DIV that is created.
+        ,   hidden = true  // Start off hidden, toggle with hide/show functions below.
+        ,   hideDelay = 200  // Delay (in ms) before the tooltip hides after calling hide().
+        ,   tooltip = null // d3 select of the tooltip div.
+        ,   lastPosition = { left: null, top: null } // Last position the tooltip was in.
+        ,   enabled = true  // True -> tooltips are rendered. False -> don't render tooltips.
+        ,   duration = 100 // Tooltip movement duration, in ms.
+        ,   headerEnabled = true // If is to show the tooltip header.
+        ,   nvPointerEventsClass = "nv-pointer-events-none" // CSS class to specify whether element should not have mouse events.
+    ;
+
+    // Format function for the tooltip values column.
+    // d is value,
+    // i is series index
+    // p is point containing the value
+    var valueFormatter = function(d, i, p) {
+        return d;
+    };
+
+    // Format function for the tooltip header value.
+    var headerFormatter = function(d) {
+        return d;
+    };
+
+    var keyFormatter = function(d, i) {
+        return d;
+    };
+
+    // By default, the tooltip model renders a beautiful table inside a DIV, returned as HTML
+    // You can override this function if a custom tooltip is desired. For instance, you could directly manipulate
+    // the DOM by accessing elem and returning false.
+    var contentGenerator = function(d, elem) {
+        if (d === null) {
+            return '';
+        }
+
+        var table = d3.select(document.createElement("table"));
+        if (headerEnabled) {
+            var theadEnter = table.selectAll("thead")
+                .data([d])
+                .enter().append("thead");
+
+            theadEnter.append("tr")
+                .append("td")
+                .attr("colspan", 3)
+                .append("strong")
+                .classed("x-value", true)
+                .html(headerFormatter(d.value));
+        }
+
+        var tbodyEnter = table.selectAll("tbody")
+            .data([d])
+            .enter().append("tbody");
+
+        var trowEnter = tbodyEnter.selectAll("tr")
+                .data(function(p) { return p.series})
+                .enter()
+                .append("tr")
+                .classed("highlight", function(p) { return p.highlight});
+
+        trowEnter.append("td")
+            .classed("legend-color-guide",true)
+            .append("div")
+            .style("background-color", function(p) { return p.color});
+
+        trowEnter.append("td")
+            .classed("key",true)
+            .classed("total",function(p) { return !!p.total})
+            .html(function(p, i) { return keyFormatter(p.key, i)});
+
+        trowEnter.append("td")
+            .classed("value",true)
+            .html(function(p, i) { return valueFormatter(p.value, i, p) });
+
+        trowEnter.filter(function (p,i) { return p.percent !== undefined }).append("td")
+            .classed("percent", true)
+            .html(function(p, i) { return "(" + d3.format('%')(p.percent) + ")" });
+
+        trowEnter.selectAll("td").each(function(p) {
+            if (p.highlight) {
+                var opacityScale = d3.scale.linear().domain([0,1]).range(["#fff",p.color]);
+                var opacity = 0.6;
+                d3.select(this)
+                    .style("border-bottom-color", opacityScale(opacity))
+                    .style("border-top-color", opacityScale(opacity))
+                ;
+            }
+        });
+
+        var html = table.node().outerHTML;
+        if (d.footer !== undefined)
+            html += "<div class='footer'>" + d.footer + "</div>";
+        return html;
+
+    };
+
+    /*
+     Function that returns the position (relative to the viewport/document.body)
+     the tooltip should be placed in.
+     Should return: {
+        left: <leftPos>,
+        top: <topPos>
+     }
+     */
+    var position = function() {
+        var pos = {
+            left: d3.event !== null ? d3.event.clientX : 0,
+            top: d3.event !== null ? d3.event.clientY : 0
+        };
+
+        if(getComputedStyle(document.body).transform != 'none') {
+            // Take the offset into account, as now the tooltip is relative
+            // to document.body.
+            var client = document.body.getBoundingClientRect();
+            pos.left -= client.left;
+            pos.top -= client.top;
+        }
+
+        return pos;
+    };
+
+    var dataSeriesExists = function(d) {
+        if (d && d.series) {
+            if (nv.utils.isArray(d.series)) {
+                return true;
+            }
+            // if object, it's okay just convert to array of the object
+            if (nv.utils.isObject(d.series)) {
+                d.series = [d.series];
+                return true;
+            }
+        }
+        return false;
+    };
+
+    // Calculates the gravity offset of the tooltip. Parameter is position of tooltip
+    // relative to the viewport.
+    var calcGravityOffset = function(pos) {
+        var height = tooltip.node().offsetHeight,
+            width = tooltip.node().offsetWidth,
+            clientWidth = document.documentElement.clientWidth, // Don't want scrollbars.
+            clientHeight = document.documentElement.clientHeight, // Don't want scrollbars.
+            left, top, tmp;
+
+        // calculate position based on gravity
+        switch (gravity) {
+            case 'e':
+                left = - width - distance;
+                top = - (height / 2);
+                if(pos.left + left < 0) left = distance;
+                if((tmp = pos.top + top) < 0) top -= tmp;
+                if((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
+                break;
+            case 'w':
+                left = distance;
+                top = - (height / 2);
+                if (pos.left + left + width > clientWidth) left = - width - distance;
+                if ((tmp = pos.top + top) < 0) top -= tmp;
+                if ((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
+                break;
+            case 'n':
+                left = - (width / 2) - 5; // - 5 is an approximation of the mouse's height.
+                top = distance;
+                if (pos.top + top + height > clientHeight) top = - height - distance;
+                if ((tmp = pos.left + left) < 0) left -= tmp;
+                if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
+                break;
+            case 's':
+                left = - (width / 2);
+                top = - height - distance;
+                if (pos.top + top < 0) top = distance;
+                if ((tmp = pos.left + left) < 0) left -= tmp;
+                if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
+                break;
+            case 'center':
+                left = - (width / 2);
+                top = - (height / 2);
+                break;
+            default:
+                left = 0;
+                top = 0;
+                break;
+        }
+
+        return { 'left': left, 'top': top };
+    };
+
+    /*
+     Positions the tooltip in the correct place, as given by the position() function.
+     */
+    var positionTooltip = function() {
+        nv.dom.read(function() {
+            var pos = position(),
+                gravityOffset = calcGravityOffset(pos),
+                left = pos.left + gravityOffset.left,
+                top = pos.top + gravityOffset.top;
+
+            // delay hiding a bit to avoid flickering
+            if (hidden) {
+                tooltip
+                    .interrupt()
+                    .transition()
+                    .delay(hideDelay)
+                    .duration(0)
+                    .style('opacity', 0);
+            } else {
+                // using tooltip.style('transform') returns values un-usable for tween
+                var old_translate = 'translate(' + lastPosition.left + 'px, ' + lastPosition.top + 'px)';
+                var new_translate = 'translate(' + Math.round(left) + 'px, ' + Math.round(top) + 'px)';
+                var translateInterpolator = d3.interpolateString(old_translate, new_translate);
+                var is_hidden = tooltip.style('opacity') < 0.1;
+
+                tooltip
+                    .interrupt() // cancel running transitions
+                    .transition()
+                    .duration(is_hidden ? 0 : duration)
+                    // using tween since some versions of d3 can't auto-tween a translate on a div
+                    .styleTween('transform', function (d) {
+                        return translateInterpolator;
+                    }, 'important')
+                    // Safari has its own `-webkit-transform` and does not support `transform`
+                    .styleTween('-webkit-transform', function (d) {
+                        return translateInterpolator;
+                    })
+                    .style('-ms-transform', new_translate)
+                    .style('opacity', 1);
+            }
+
+            lastPosition.left = left;
+            lastPosition.top = top;
+        });
+    };
+
+    // Creates new tooltip container, or uses existing one on DOM.
+    function initTooltip() {
+        if (!tooltip || !tooltip.node()) {
+            // Create new tooltip div if it doesn't exist on DOM.
+
+            var data = [1];
+            tooltip = d3.select(document.body).selectAll('#'+id).data(data);
+
+            tooltip.enter().append('div')
+                   .attr("class", "nvtooltip " + (classes ? classes : "xy-tooltip"))
+                   .attr("id", id)
+                   .style("top", 0).style("left", 0)
+                   .style('opacity', 0)
+                   .style('position', 'absolute')
+                   .selectAll("div, table, td, tr").classed(nvPointerEventsClass, true)
+                   .classed(nvPointerEventsClass, true);
+
+            tooltip.exit().remove()
+        }
+    }
+
+    // Draw the tooltip onto the DOM.
+    function nvtooltip() {
+        if (!enabled) return;
+        if (!dataSeriesExists(data)) return;
+
+        nv.dom.write(function () {
+            initTooltip();
+            // Generate data and set it into tooltip.
+            // Bonus - If you override contentGenerator and return false, you can use something like
+            //         Angular, React or Knockout to bind the data for your tooltip directly to the DOM.
+            var newContent = contentGenerator(data, tooltip.node());
+            if (newContent) {
+                tooltip.node().innerHTML = newContent;
+            }
+
+            positionTooltip();
+        });
+
+        return nvtooltip;
+    }
+
+    nvtooltip.nvPointerEventsClass = nvPointerEventsClass;
+    nvtooltip.options = nv.utils.optionsFunc.bind(nvtooltip);
+
+    nvtooltip._options = Object.create({}, {
+        // simple read/write options
+        duration: {get: function(){return duration;}, set: function(_){duration=_;}},
+        gravity: {get: function(){return gravity;}, set: function(_){gravity=_;}},
+        distance: {get: function(){return distance;}, set: function(_){distance=_;}},
+        snapDistance: {get: function(){return snapDistance;}, set: function(_){snapDistance=_;}},
+        classes: {get: function(){return classes;}, set: function(_){classes=_;}},
+        enabled: {get: function(){return enabled;}, set: function(_){enabled=_;}},
+        hideDelay: {get: function(){return hideDelay;}, set: function(_){hideDelay=_;}},
+        contentGenerator: {get: function(){return contentGenerator;}, set: function(_){contentGenerator=_;}},
+        valueFormatter: {get: function(){return valueFormatter;}, set: function(_){valueFormatter=_;}},
+        headerFormatter: {get: function(){return headerFormatter;}, set: function(_){headerFormatter=_;}},
+        keyFormatter: {get: function(){return keyFormatter;}, set: function(_){keyFormatter=_;}},
+        headerEnabled: {get: function(){return headerEnabled;}, set: function(_){headerEnabled=_;}},
+        position: {get: function(){return position;}, set: function(_){position=_;}},
+
+        // Deprecated options
+        chartContainer: {get: function(){return document.body;}, set: function(_){
+            // deprecated after 1.8.3
+            nv.deprecated('chartContainer', 'feature removed after 1.8.3');
+        }},
+        fixedTop: {get: function(){return null;}, set: function(_){
+            // deprecated after 1.8.1
+            nv.deprecated('fixedTop', 'feature removed after 1.8.1');
+        }},
+        offset: {get: function(){return {left: 0, top: 0};}, set: function(_){
+            // deprecated after 1.8.1
+            nv.deprecated('offset', 'use chart.tooltip.distance() instead');
+        }},
+
+        // options with extra logic
+        hidden: {get: function(){return hidden;}, set: function(_){
+            if (hidden != _) {
+                hidden = !!_;
+                nvtooltip();
+            }
+        }},
+        data: {get: function(){return data;}, set: function(_){
+            // if showing a single data point, adjust data format with that
+            if (_.point) {
+                _.value = _.point.x;
+                _.series = _.series || {};
+                _.series.value = _.point.y;
+                _.series.color = _.point.color || _.series.color;
+            }
+            data = _;
+        }},
+
+        // read only properties
+        node: {get: function(){return tooltip.node();}, set: function(_){}},
+        id: {get: function(){return id;}, set: function(_){}}
+    });
+
+    nv.utils.initOptions(nvtooltip);
+    return nvtooltip;
+};
+
+
+/*
+Gets the browser window size
+
+Returns object with height and width properties
+ */
+nv.utils.windowSize = function() {
+    // Sane defaults
+    var size = {width: 640, height: 480};
+
+    // Most recent browsers use
+    if (window.innerWidth && window.innerHeight) {
+        size.width = window.innerWidth;
+        size.height = window.innerHeight;
+        return (size);
+    }
+
+    // IE can use depending on mode it is in
+    if (document.compatMode=='CSS1Compat' &&
+        document.documentElement &&
+        document.documentElement.offsetWidth ) {
+
+        size.width = document.documentElement.offsetWidth;
+        size.height = document.documentElement.offsetHeight;
+        return (size);
+    }
+
+    // Earlier IE uses Doc.body
+    if (document.body && document.body.offsetWidth) {
+        size.width = document.body.offsetWidth;
+        size.height = document.body.offsetHeight;
+        return (size);
+    }
+
+    return (size);
+};
+
+
+/* handle dumb browser quirks...  isinstance breaks if you use frames
+typeof returns 'object' for null, NaN is a number, etc.
+ */
+nv.utils.isArray = Array.isArray;
+nv.utils.isObject = function(a) {
+    return a !== null && typeof a === 'object';
+};
+nv.utils.isFunction = function(a) {
+    return typeof a === 'function';
+};
+nv.utils.isDate = function(a) {
+    return toString.call(a) === '[object Date]';
+};
+nv.utils.isNumber = function(a) {
+    return !isNaN(a) && typeof a === 'number';
+};
+
+
+/*
+Binds callback function to run when window is resized
+ */
+nv.utils.windowResize = function(handler) {
+    if (window.addEventListener) {
+        window.addEventListener('resize', handler);
+    } else {
+        nv.log("ERROR: Failed to bind to window.resize with: ", handler);
+    }
+    // return object with clear function to remove the single added callback.
+    return {
+        callback: handler,
+        clear: function() {
+            window.removeEventListener('resize', handler);
+        }
+    }
+};
+
+
+/*
+Backwards compatible way to implement more d3-like coloring of graphs.
+Can take in nothing, an array, or a function/scale
+To use a normal scale, get the range and pass that because we must be able
+to take two arguments and use the index to keep backward compatibility
+*/
+nv.utils.getColor = function(color) {
+    //if you pass in nothing, get default colors back
+    if (color === undefined) {
+        return nv.utils.defaultColor();
+
+    //if passed an array, turn it into a color scale
+    } else if(nv.utils.isArray(color)) {
+        var color_scale = d3.scale.ordinal().range(color);
+        return function(d, i) {
+            var key = i === undefined ? d : i;
+            return d.color || color_scale(key);
+        };
+
+    //if passed a function or scale, return it, or whatever it may be
+    //external libs, such as angularjs-nvd3-directives use this
+    } else {
+        //can't really help it if someone passes rubbish as color
+        return color;
+    }
+};
+
+
+/*
+Default color chooser uses a color scale of 20 colors from D3
+ https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors
+ */
+nv.utils.defaultColor = function() {
+    // get range of the scale so we'll turn it into our own function.
+    return nv.utils.getColor(d3.scale.category20().range());
+};
+
+
+/*
+Returns a color function that takes the result of 'getKey' for each series and
+looks for a corresponding color from the dictionary
+*/
+nv.utils.customTheme = function(dictionary, getKey, defaultColors) {
+    // use default series.key if getKey is undefined
+    getKey = getKey || function(series) { return series.key };
+    defaultColors = defaultColors || d3.scale.category20().range();
+
+    // start at end of default color list and walk back to index 0
+    var defIndex = defaultColors.length;
+
+    return function(series, index) {
+        var key = getKey(series);
+        if (nv.utils.isFunction(dictionary[key])) {
+            return dictionary[key]();
+        } else if (dictionary[key] !== undefined) {
+            return dictionary[key];
+        } else {
+            // no match in dictionary, use a default color
+            if (!defIndex) {
+                // used all the default colors, start over
+                defIndex = defaultColors.length;
+            }
+            defIndex = defIndex - 1;
+            return defaultColors[defIndex];
+        }
+    };
+};
+
+
+/*
+From the PJAX example on d3js.org, while this is not really directly needed
+it's a very cool method for doing pjax, I may expand upon it a little bit,
+open to suggestions on anything that may be useful
+*/
+nv.utils.pjax = function(links, content) {
+
+    var load = function(href) {
+        d3.html(href, function(fragment) {
+            var target = d3.select(content).node();
+            target.parentNode.replaceChild(
+                d3.select(fragment).select(content).node(),
+                target);
+            nv.utils.pjax(links, content);
+        });
+    };
+
+    d3.selectAll(links).on("click", function() {
+        history.pushState(this.href, this.textContent, this.href);
+        load(this.href);
+        d3.event.preventDefault();
+    });
+
+    d3.select(window).on("popstate", function() {
+        if (d3.event.state) {
+            load(d3.event.state);
+        }
+    });
+};
+
+
+/*
+For when we want to approximate the width in pixels for an SVG:text element.
+Most common instance is when the element is in a display:none; container.
+Forumla is : text.length * font-size * constant_factor
+*/
+nv.utils.calcApproxTextWidth = function (svgTextElem) {
+    if (nv.utils.isFunction(svgTextElem.style) && nv.utils.isFunction(svgTextElem.text)) {
+        var fontSize = parseInt(svgTextElem.style("font-size").replace("px",""), 10);
+        var textLength = svgTextElem.text().length;
+        return nv.utils.NaNtoZero(textLength * fontSize * 0.5);
+    }
+    return 0;
+};
+
+
+/*
+Numbers that are undefined, null or NaN, convert them to zeros.
+*/
+nv.utils.NaNtoZero = function(n) {
+    if (!nv.utils.isNumber(n)
+        || isNaN(n)
+        || n === null
+        || n === Infinity
+        || n === -Infinity) {
+
+        return 0;
+    }
+    return n;
+};
+
+/*
+Add a way to watch for d3 transition ends to d3
+*/
+d3.selection.prototype.watchTransition = function(renderWatch){
+    var args = [this].concat([].slice.call(arguments, 1));
+    return renderWatch.transition.apply(renderWatch, args);
+};
+
+
+/*
+Helper object to watch when d3 has rendered something
+*/
+nv.utils.renderWatch = function(dispatch, duration) {
+    if (!(this instanceof nv.utils.renderWatch)) {
+        return new nv.utils.renderWatch(dispatch, duration);
+    }
+
+    var _duration = duration !== undefined ? duration : 250;
+    var renderStack = [];
+    var self = this;
+
+    this.models = function(models) {
+        models = [].slice.call(arguments, 0);
+        models.forEach(function(model){
+            model.__rendered = false;
+            (function(m){
+                m.dispatch.on('renderEnd', function(arg){
+                    m.__rendered = true;
+                    self.renderEnd('model');
+                });
+            })(model);
+
+            if (renderStack.indexOf(model) < 0) {
+                renderStack.push(model);
+            }
+        });
+    return this;
+    };
+
+    this.reset = function(duration) {
+        if (duration !== undefined) {
+            _duration = duration;
+        }
+        renderStack = [];
+    };
+
+    this.transition = function(selection, args, duration) {
+        args = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
+
+        if (args.length > 1) {
+            duration = args.pop();
+        } else {
+            duration = _duration !== undefined ? _duration : 250;
+        }
+        selection.__rendered = false;
+
+        if (renderStack.indexOf(selection) < 0) {
+            renderStack.push(selection);
+        }
+
+        if (duration === 0) {
+            selection.__rendered = true;
+            selection.delay = function() { return this; };
+            selection.duration = function() { return this; };
+            return selection;
+        } else {
+            if (selection.length === 0) {
+                selection.__rendered = true;
+            } else if (selection.every( function(d){ return !d.length; } )) {
+                selection.__rendered = true;
+            } else {
+                selection.__rendered = false;
+            }
+
+            var n = 0;
+            return selection
+                .transition()
+                .duration(duration)
+                .each(function(){ ++n; })
+                .each('end', function(d, i) {
+                    if (--n === 0) {
+                        selection.__rendered = true;
+                        self.renderEnd.apply(this, args);
+                    }
+                });
+        }
+    };
+
+    this.renderEnd = function() {
+        if (renderStack.every( function(d){ return d.__rendered; } )) {
+            renderStack.forEach( function(d){ d.__rendered = false; });
+            dispatch.renderEnd.apply(this, arguments);
+        }
+    }
+
+};
+
+
+/*
+Takes multiple objects and combines them into the first one (dst)
+example:  nv.utils.deepExtend({a: 1}, {a: 2, b: 3}, {c: 4});
+gives:  {a: 2, b: 3, c: 4}
+*/
+nv.utils.deepExtend = function(dst){
+    var sources = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
+    sources.forEach(function(source) {
+        for (var key in source) {
+            var isArray = nv.utils.isArray(dst[key]);
+            var isObject = nv.utils.isObject(dst[key]);
+            var srcObj = nv.utils.isObject(source[key]);
+
+            if (isObject && !isArray && srcObj) {
+                nv.utils.deepExtend(dst[key], source[key]);
+            } else {
+                dst[key] = source[key];
+            }
+        }
+    });
+};
+
+
+/*
+state utility object, used to track d3 states in the models
+*/
+nv.utils.state = function(){
+    if (!(this instanceof nv.utils.state)) {
+        return new nv.utils.state();
+    }
+    var state = {};
+    var _self = this;
+    var _setState = function(){};
+    var _getState = function(){ return {}; };
+    var init = null;
+    var changed = null;
+
+    this.dispatch = d3.dispatch('change', 'set');
+
+    this.dispatch.on('set', function(state){
+        _setState(state, true);
+    });
+
+    this.getter = function(fn){
+        _getState = fn;
+        return this;
+    };
+
+    this.setter = function(fn, callback) {
+        if (!callback) {
+            callback = function(){};
+        }
+        _setState = function(state, update){
+            fn(state);
+            if (update) {
+                callback();
+            }
+        };
+        return this;
+    };
+
+    this.init = function(state){
+        init = init || {};
+        nv.utils.deepExtend(init, state);
+    };
+
+    var _set = function(){
+        var settings = _getState();
+
+        if (JSON.stringify(settings) === JSON.stringify(state)) {
+            return false;
+        }
+
+        for (var key in settings) {
+            if (state[key] === undefined) {
+                state[key] = {};
+            }
+            state[key] = settings[key];
+            changed = true;
+        }
+        return true;
+    };
+
+    this.update = function(){
+        if (init) {
+            _setState(init, false);
+            init = null;
+        }
+        if (_set.call(this)) {
+            this.dispatch.change(state);
+        }
+    };
+
+};
+
+
+/*
+Snippet of code you can insert into each nv.models.* to give you the ability to
+do things like:
+chart.options({
+  showXAxis: true,
+  tooltips: true
+});
+
+To enable in the chart:
+chart.options = nv.utils.optionsFunc.bind(chart);
+*/
+nv.utils.optionsFunc = function(args) {
+    if (args) {
+        d3.map(args).forEach((function(key,value) {
+            if (nv.utils.isFunction(this[key])) {
+                this[key](value);
+            }
+        }).bind(this));
+    }
+    return this;
+};
+
+
+/*
+numTicks:  requested number of ticks
+data:  the chart data
+
+returns the number of ticks to actually use on X axis, based on chart data
+to avoid duplicate ticks with the same value
+*/
+nv.utils.calcTicksX = function(numTicks, data) {
+    // find max number of values from all data streams
+    var numValues = 1;
+    var i = 0;
+    for (i; i < data.length; i += 1) {
+        var stream_len = data[i] && data[i].values ? data[i].values.length : 0;
+        numValues = stream_len > numValues ? stream_len : numValues;
+    }
+    nv.log("Requested number of ticks: ", numTicks);
+    nv.log("Calculated max values to be: ", numValues);
+    // make sure we don't have more ticks than values to avoid duplicates
+    numTicks = numTicks > numValues ? numTicks = numValues - 1 : numTicks;
+    // make sure we have at least one tick
+    numTicks = numTicks < 1 ? 1 : numTicks;
+    // make sure it's an integer
+    numTicks = Math.floor(numTicks);
+    nv.log("Calculating tick count as: ", numTicks);
+    return numTicks;
+};
+
+
+/*
+returns number of ticks to actually use on Y axis, based on chart data
+*/
+nv.utils.calcTicksY = function(numTicks, data) {
+    // currently uses the same logic but we can adjust here if needed later
+    return nv.utils.calcTicksX(numTicks, data);
+};
+
+
+/*
+Add a particular option from an options object onto chart
+Options exposed on a chart are a getter/setter function that returns chart
+on set to mimic typical d3 option chaining, e.g. svg.option1('a').option2('b');
+
+option objects should be generated via Object.create() to provide
+the option of manipulating data via get/set functions.
+*/
+nv.utils.initOption = function(chart, name) {
+    // if it's a call option, just call it directly, otherwise do get/set
+    if (chart._calls && chart._calls[name]) {
+        chart[name] = chart._calls[name];
+    } else {
+        chart[name] = function (_) {
+            if (!arguments.length) return chart._options[name];
+            chart._overrides[name] = true;
+            chart._options[name] = _;
+            return chart;
+        };
+        // calling the option as _option will ignore if set by option already
+        // so nvd3 can set options internally but the stop if set manually
+        chart['_' + name] = function(_) {
+            if (!arguments.length) return chart._options[name];
+            if (!chart._overrides[name]) {
+                chart._options[name] = _;
+            }
+            return chart;
+        }
+    }
+};
+
+
+/*
+Add all options in an options object to the chart
+*/
+nv.utils.initOptions = function(chart) {
+    chart._overrides = chart._overrides || {};
+    var ops = Object.getOwnPropertyNames(chart._options || {});
+    var calls = Object.getOwnPropertyNames(chart._calls || {});
+    ops = ops.concat(calls);
+    for (var i in ops) {
+        nv.utils.initOption(chart, ops[i]);
+    }
+};
+
+
+/*
+Inherit options from a D3 object
+d3.rebind makes calling the function on target actually call it on source
+Also use _d3options so we can track what we inherit for documentation and chained inheritance
+*/
+nv.utils.inheritOptionsD3 = function(target, d3_source, oplist) {
+    target._d3options = oplist.concat(target._d3options || []);
+    // Find unique d3 options (string) and update d3options
+    target._d3options = (target._d3options || []).filter(function(item, i, ar){ return ar.indexOf(item) === i; });
+    oplist.unshift(d3_source);
+    oplist.unshift(target);
+    d3.rebind.apply(this, oplist);
+};
+
+
+/*
+Remove duplicates from an array
+*/
+nv.utils.arrayUnique = function(a) {
+    return a.sort().filter(function(item, pos) {
+        return !pos || item != a[pos - 1];
+    });
+};
+
+
+/*
+Keeps a list of custom symbols to draw from in addition to d3.svg.symbol
+Necessary since d3 doesn't let you extend its list -_-
+Add new symbols by doing nv.utils.symbols.set('name', function(size){...});
+*/
+nv.utils.symbolMap = d3.map();
+
+
+/*
+Replaces d3.svg.symbol so that we can look both there and our own map
+ */
+nv.utils.symbol = function() {
+    var type,
+        size = 64;
+    function symbol(d,i) {
+        var t = type.call(this,d,i);
+        var s = size.call(this,d,i);
+        if (d3.svg.symbolTypes.indexOf(t) !== -1) {
+            return d3.svg.symbol().type(t).size(s)();
+        } else {
+            return nv.utils.symbolMap.get(t)(s);
+        }
+    }
+    symbol.type = function(_) {
+        if (!arguments.length) return type;
+        type = d3.functor(_);
+        return symbol;
+    };
+    symbol.size = function(_) {
+        if (!arguments.length) return size;
+        size = d3.functor(_);
+        return symbol;
+    };
+    return symbol;
+};
+
+
+/*
+Inherit option getter/setter functions from source to target
+d3.rebind makes calling the function on target actually call it on source
+Also track via _inherited and _d3options so we can track what we inherit
+for documentation generation purposes and chained inheritance
+*/
+nv.utils.inheritOptions = function(target, source) {
+    // inherit all the things
+    var ops = Object.getOwnPropertyNames(source._options || {});
+    var calls = Object.getOwnPropertyNames(source._calls || {});
+    var inherited = source._inherited || [];
+    var d3ops = source._d3options || [];
+    var args = ops.concat(calls).concat(inherited).concat(d3ops);
+    args.unshift(source);
+    args.unshift(target);
+    d3.rebind.apply(this, args);
+    // pass along the lists to keep track of them, don't allow duplicates
+    target._inherited = nv.utils.arrayUnique(ops.concat(calls).concat(inherited).concat(ops).concat(target._inherited || []));
+    target._d3options = nv.utils.arrayUnique(d3ops.concat(target._d3options || []));
+};
+
+
+/*
+Runs common initialize code on the svg before the chart builds
+*/
+nv.utils.initSVG = function(svg) {
+    svg.classed({'nvd3-svg':true});
+};
+
+
+/*
+Sanitize and provide default for the container height.
+*/
+nv.utils.sanitizeHeight = function(height, container) {
+    return (height || parseInt(container.style('height'), 10) || 400);
+};
+
+
+/*
+Sanitize and provide default for the container width.
+*/
+nv.utils.sanitizeWidth = function(width, container) {
+    return (width || parseInt(container.style('width'), 10) || 960);
+};
+
+
+/*
+Calculate the available height for a chart.
+*/
+nv.utils.availableHeight = function(height, container, margin) {
+    return Math.max(0,nv.utils.sanitizeHeight(height, container) - margin.top - margin.bottom);
+};
+
+/*
+Calculate the available width for a chart.
+*/
+nv.utils.availableWidth = function(width, container, margin) {
+    return Math.max(0,nv.utils.sanitizeWidth(width, container) - margin.left - margin.right);
+};
+
+/*
+Clear any rendered chart components and display a chart's 'noData' message
+*/
+nv.utils.noData = function(chart, container) {
+    var opt = chart.options(),
+        margin = opt.margin(),
+        noData = opt.noData(),
+        data = (noData == null) ? ["No Data Available."] : [noData],
+        height = nv.utils.availableHeight(null, container, margin),
+        width = nv.utils.availableWidth(null, container, margin),
+        x = margin.left + width/2,
+        y = margin.top + height/2;
+
+    //Remove any previously created chart components
+    container.selectAll('g').remove();
+
+    var noDataText = container.selectAll('.nv-noData').data(data);
+
+    noDataText.enter().append('text')
+        .attr('class', 'nvd3 nv-noData')
+        .attr('dy', '-.7em')
+        .style('text-anchor', 'middle');
+
+    noDataText
+        .attr('x', x)
+        .attr('y', y)
+        .text(function(t){ return t; });
+};
+
+/*
+ Wrap long labels.
+ */
+nv.utils.wrapTicks = function (text, width) {
+    text.each(function() {
+        var text = d3.select(this),
+            words = text.text().split(/\s+/).reverse(),
+            word,
+            line = [],
+            lineNumber = 0,
+            lineHeight = 1.1,
+            y = text.attr("y"),
+            dy = parseFloat(text.attr("dy")),
+            tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
+        while (word = words.pop()) {
+            line.push(word);
+            tspan.text(line.join(" "));
+            if (tspan.node().getComputedTextLength() > width) {
+                line.pop();
+                tspan.text(line.join(" "));
+                line = [word];
+                tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
+            }
+        }
+    });
+};
+
+/*
+Check equality of 2 array
+*/
+nv.utils.arrayEquals = function (array1, array2) {
+    if (array1 === array2)
+        return true;
+
+    if (!array1 || !array2)
+        return false;
+
+    // compare lengths - can save a lot of time
+    if (array1.length != array2.length)
+        return false;
+
+    for (var i = 0,
+        l = array1.length; i < l; i++) {
+        // Check if we have nested arrays
+        if (array1[i] instanceof Array && array2[i] instanceof Array) {
+            // recurse into the nested arrays
+            if (!nv.arrayEquals(array1[i], array2[i]))
+                return false;
+        } else if (array1[i] != array2[i]) {
+            // Warning - two different object instances will never be equal: {x:20} != {x:20}
+            return false;
+        }
+    }
+    return true;
+};
+
+/*
+ Check if a point within an arc
+ */
+nv.utils.pointIsInArc = function(pt, ptData, d3Arc) {
+    // Center of the arc is assumed to be 0,0
+    // (pt.x, pt.y) are assumed to be relative to the center
+    var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
+      r2 = d3Arc.outerRadius()(ptData),
+      theta1 = d3Arc.startAngle()(ptData),
+      theta2 = d3Arc.endAngle()(ptData);
+
+    var dist = pt.x * pt.x + pt.y * pt.y,
+      angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system.
+
+    angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
+
+    return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
+      (theta1 <= angle) && (angle <= theta2);
+};
+
+nv.models.axis = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var axis = d3.svg.axis();
+    var scale = d3.scale.linear();
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , width = 75 //only used for tickLabel currently
+        , height = 60 //only used for tickLabel currently
+        , axisLabelText = null
+        , showMaxMin = true //TODO: showMaxMin should be disabled on all ordinal scaled axes
+        , rotateLabels = 0
+        , rotateYLabel = true
+        , staggerLabels = false
+        , isOrdinal = false
+        , ticks = null
+        , axisLabelDistance = 0
+        , fontSize = undefined
+        , duration = 250
+        , dispatch = d3.dispatch('renderEnd')
+        , tickFormatMaxMin
+        ;
+    axis
+        .scale(scale)
+        .orient('bottom')
+        .tickFormat(function(d) { return d })
+    ;
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var scale0;
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        selection.each(function(data) {
+            var container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-axis').data([data]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-axis');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            if (ticks !== null)
+                axis.ticks(ticks);
+            else if (axis.orient() == 'top' || axis.orient() == 'bottom')
+                axis.ticks(Math.abs(scale.range()[1] - scale.range()[0]) / 100);
+
+            //TODO: consider calculating width/height based on whether or not label is added, for reference in charts using this component
+            g.watchTransition(renderWatch, 'axis').call(axis);
+
+            scale0 = scale0 || axis.scale();
+
+            var fmt = axis.tickFormat();
+            if (fmt == null) {
+                fmt = scale0.tickFormat();
+            }
+
+            var axisLabel = g.selectAll('text.nv-axislabel')
+                .data([axisLabelText || null]);
+            axisLabel.exit().remove();
+
+            //only skip when fontSize is undefined so it can be cleared with a null or blank string
+            if (fontSize !== undefined) {
+                g.selectAll('g').select("text").style('font-size', fontSize);
+            }
+
+            var xLabelMargin;
+            var axisMaxMin;
+            var w;
+            switch (axis.orient()) {
+                case 'top':
+                    axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+                  w = 0;
+                  if (scale.range().length === 1) {
+                    w = isOrdinal ? scale.range()[0] * 2 + scale.rangeBand() : 0;
+                  } else if (scale.range().length === 2) {
+                    w = isOrdinal ? scale.range()[0] + scale.range()[1] + scale.rangeBand() : scale.range()[1];
+                  } else if ( scale.range().length > 2){
+                    w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]);
+                  };
+                    axisLabel
+                        .attr('text-anchor', 'middle')
+                        .attr('y', 0)
+                        .attr('x', w/2);
+                    if (showMaxMin) {
+                        axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+                            .data(scale.domain());
+                        axisMaxMin.enter().append('g').attr('class',function(d,i){
+                                return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ')
+                        }).append('text');
+                        axisMaxMin.exit().remove();
+                        axisMaxMin
+                            .attr('transform', function(d,i) {
+                                return 'translate(' + nv.utils.NaNtoZero(scale(d)) + ',0)'
+                            })
+                            .select('text')
+                            .attr('dy', '-0.5em')
+                            .attr('y', -axis.tickPadding())
+                            .attr('text-anchor', 'middle')
+                            .text(function(d,i) {
+                                var formatter = tickFormatMaxMin || fmt;
+                                var v = formatter(d);
+                                return ('' + v).match('NaN') ? '' : v;
+                            });
+                        axisMaxMin.watchTransition(renderWatch, 'min-max top')
+                            .attr('transform', function(d,i) {
+                                return 'translate(' + nv.utils.NaNtoZero(scale.range()[i]) + ',0)'
+                            });
+                    }
+                    break;
+                case 'bottom':
+                    xLabelMargin = axisLabelDistance + 36;
+                    var maxTextWidth = 30;
+                    var textHeight = 0;
+                    var xTicks = g.selectAll('g').select("text");
+                    var rotateLabelsRule = '';
+                    if (rotateLabels%360) {
+                        //Reset transform on ticks so textHeight can be calculated correctly
+                        xTicks.attr('transform', '');
+                        //Calculate the longest xTick width
+                        xTicks.each(function(d,i){
+                            var box = this.getBoundingClientRect();
+                            var width = box.width;
+                            textHeight = box.height;
+                            if(width > maxTextWidth) maxTextWidth = width;
+                        });
+                        rotateLabelsRule = 'rotate(' + rotateLabels + ' 0,' + (textHeight/2 + axis.tickPadding()) + ')';
+                        //Convert to radians before calculating sin. Add 30 to margin for healthy padding.
+                        var sin = Math.abs(Math.sin(rotateLabels*Math.PI/180));
+                        xLabelMargin = (sin ? sin*maxTextWidth : maxTextWidth)+30;
+                        //Rotate all xTicks
+                        xTicks
+                            .attr('transform', rotateLabelsRule)
+                            .style('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end');
+                    } else {
+                        if (staggerLabels) {
+                            xTicks
+                                .attr('transform', function(d,i) {
+                                    return 'translate(0,' + (i % 2 == 0 ? '0' : '12') + ')'
+                                });
+                        } else {
+                            xTicks.attr('transform', "translate(0,0)");
+                        }
+                    }
+                    axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+                    w = 0;
+                    if (scale.range().length === 1) {
+                        w = isOrdinal ? scale.range()[0] * 2 + scale.rangeBand() : 0;
+                    } else if (scale.range().length === 2) {
+                        w = isOrdinal ? scale.range()[0] + scale.range()[1] + scale.rangeBand() : scale.range()[1];
+                    } else if ( scale.range().length > 2){
+                        w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]);
+                    };
+                    axisLabel
+                        .attr('text-anchor', 'middle')
+                        .attr('y', xLabelMargin)
+                        .attr('x', w/2);
+                    if (showMaxMin) {
+                        //if (showMaxMin && !isOrdinal) {
+                        axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+                            //.data(scale.domain())
+                            .data([scale.domain()[0], scale.domain()[scale.domain().length - 1]]);
+                        axisMaxMin.enter().append('g').attr('class',function(d,i){
+                                return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ')
+                        }).append('text');
+                        axisMaxMin.exit().remove();
+                        axisMaxMin
+                            .attr('transform', function(d,i) {
+                                return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)'
+                            })
+                            .select('text')
+                            .attr('dy', '.71em')
+                            .attr('y', axis.tickPadding())
+                            .attr('transform', rotateLabelsRule)
+                            .style('text-anchor', rotateLabels ? (rotateLabels%360 > 0 ? 'start' : 'end') : 'middle')
+                            .text(function(d,i) {
+                                var formatter = tickFormatMaxMin || fmt;
+                                var v = formatter(d);
+                                return ('' + v).match('NaN') ? '' : v;
+                            });
+                        axisMaxMin.watchTransition(renderWatch, 'min-max bottom')
+                            .attr('transform', function(d,i) {
+                                return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)'
+                            });
+                    }
+
+                    break;
+                case 'right':
+                    axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+                    axisLabel
+                        .style('text-anchor', rotateYLabel ? 'middle' : 'begin')
+                        .attr('transform', rotateYLabel ? 'rotate(90)' : '')
+                        .attr('y', rotateYLabel ? (-Math.max(margin.right, width) + 12 - (axisLabelDistance || 0)) : -10) //TODO: consider calculating this based on largest tick width... OR at least expose this on chart
+                        .attr('x', rotateYLabel ? (d3.max(scale.range()) / 2) : axis.tickPadding());
+                    if (showMaxMin) {
+                        axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+                            .data(scale.domain());
+                        axisMaxMin.enter().append('g').attr('class',function(d,i){
+                                return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ')
+                        }).append('text')
+                            .style('opacity', 0);
+                        axisMaxMin.exit().remove();
+                        axisMaxMin
+                            .attr('transform', function(d,i) {
+                                return 'translate(0,' + nv.utils.NaNtoZero(scale(d)) + ')'
+                            })
+                            .select('text')
+                            .attr('dy', '.32em')
+                            .attr('y', 0)
+                            .attr('x', axis.tickPadding())
+                            .style('text-anchor', 'start')
+                            .text(function(d, i) {
+                                var formatter = tickFormatMaxMin || fmt;
+                                var v = formatter(d);
+                                return ('' + v).match('NaN') ? '' : v;
+                            });
+                        axisMaxMin.watchTransition(renderWatch, 'min-max right')
+                            .attr('transform', function(d,i) {
+                                return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')'
+                            })
+                            .select('text')
+                            .style('opacity', 1);
+                    }
+                    break;
+                case 'left':
+                    /*
+                     //For dynamically placing the label. Can be used with dynamically-sized chart axis margins
+                     var yTicks = g.selectAll('g').select("text");
+                     yTicks.each(function(d,i){
+                     var labelPadding = this.getBoundingClientRect().width + axis.tickPadding() + 16;
+                     if(labelPadding > width) width = labelPadding;
+                     });
+                     */
+                    axisLabel.enter().append('text').attr('class', 'nv-axislabel');
+                    axisLabel
+                        .style('text-anchor', rotateYLabel ? 'middle' : 'end')
+                        .attr('transform', rotateYLabel ? 'rotate(-90)' : '')
+                        .attr('y', rotateYLabel ? (-Math.max(margin.left, width) + 25 - (axisLabelDistance || 0)) : -10)
+                        .attr('x', rotateYLabel ? (-d3.max(scale.range()) / 2) : -axis.tickPadding());
+                    if (showMaxMin) {
+                        axisMaxMin = wrap.selectAll('g.nv-axisMaxMin')
+                            .data(scale.domain());
+                        axisMaxMin.enter().append('g').attr('class',function(d,i){
+                                return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ')
+                        }).append('text')
+                            .style('opacity', 0);
+                        axisMaxMin.exit().remove();
+                        axisMaxMin
+                            .attr('transform', function(d,i) {
+                                return 'translate(0,' + nv.utils.NaNtoZero(scale0(d)) + ')'
+                            })
+                            .select('text')
+                            .attr('dy', '.32em')
+                            .attr('y', 0)
+                            .attr('x', -axis.tickPadding())
+                            .attr('text-anchor', 'end')
+                            .text(function(d,i) {
+                                var formatter = tickFormatMaxMin || fmt;
+                                var v = formatter(d);
+                                return ('' + v).match('NaN') ? '' : v;
+                            });
+                        axisMaxMin.watchTransition(renderWatch, 'min-max right')
+                            .attr('transform', function(d,i) {
+                                return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')'
+                            })
+                            .select('text')
+                            .style('opacity', 1);
+                    }
+                    break;
+            }
+            axisLabel.text(function(d) { return d });
+
+            if (showMaxMin && (axis.orient() === 'left' || axis.orient() === 'right')) {
+                //check if max and min overlap other values, if so, hide the values that overlap
+                g.selectAll('g') // the g's wrapping each tick
+                    .each(function(d,i) {
+                        d3.select(this).select('text').attr('opacity', 1);
+                        if (scale(d) < scale.range()[1] + 10 || scale(d) > scale.range()[0] - 10) { // 10 is assuming text height is 16... if d is 0, leave it!
+                            if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL
+                                d3.select(this).attr('opacity', 0);
+
+                            d3.select(this).select('text').attr('opacity', 0); // Don't remove the ZERO line!!
+                        }
+                    });
+
+                //if Max and Min = 0 only show min, Issue #281
+                if (scale.domain()[0] == scale.domain()[1] && scale.domain()[0] == 0) {
+                    wrap.selectAll('g.nv-axisMaxMin').style('opacity', function (d, i) {
+                        return !i ? 1 : 0
+                    });
+                }
+            }
+
+            if (showMaxMin && (axis.orient() === 'top' || axis.orient() === 'bottom')) {
+                var maxMinRange = [];
+                wrap.selectAll('g.nv-axisMaxMin')
+                    .each(function(d,i) {
+                        try {
+                            if (i) // i== 1, max position
+                                maxMinRange.push(scale(d) - this.getBoundingClientRect().width - 4);  //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case)
+                            else // i==0, min position
+                                maxMinRange.push(scale(d) + this.getBoundingClientRect().width + 4)
+                        }catch (err) {
+                            if (i) // i== 1, max position
+                                maxMinRange.push(scale(d) - 4);  //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case)
+                            else // i==0, min position
+                                maxMinRange.push(scale(d) + 4);
+                        }
+                    });
+                // the g's wrapping each tick
+                g.selectAll('g').each(function(d, i) {
+                    if (scale(d) < maxMinRange[0] || scale(d) > maxMinRange[1]) {
+                        if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL
+                            d3.select(this).remove();
+                        else
+                            d3.select(this).select('text').remove(); // Don't remove the ZERO line!!
+                    }
+                });
+            }
+
+            //Highlight zero tick line
+            g.selectAll('.tick')
+                .filter(function (d) {
+                    /*
+                    The filter needs to return only ticks at or near zero.
+                    Numbers like 0.00001 need to count as zero as well,
+                    and the arithmetic trick below solves that.
+                    */
+                    return !parseFloat(Math.round(d * 100000) / 1000000) && (d !== undefined)
+                })
+                .classed('zero', true);
+
+            //store old scales for use in transitions on update
+            scale0 = scale.copy();
+
+        });
+
+        renderWatch.renderEnd('axis immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    // expose chart's sub-components
+    chart.axis = axis;
+    chart.dispatch = dispatch;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        axisLabelDistance: {get: function(){return axisLabelDistance;}, set: function(_){axisLabelDistance=_;}},
+        staggerLabels:     {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+        rotateLabels:      {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
+        rotateYLabel:      {get: function(){return rotateYLabel;}, set: function(_){rotateYLabel=_;}},
+        showMaxMin:        {get: function(){return showMaxMin;}, set: function(_){showMaxMin=_;}},
+        axisLabel:         {get: function(){return axisLabelText;}, set: function(_){axisLabelText=_;}},
+        height:            {get: function(){return height;}, set: function(_){height=_;}},
+        ticks:             {get: function(){return ticks;}, set: function(_){ticks=_;}},
+        width:             {get: function(){return width;}, set: function(_){width=_;}},
+        fontSize:          {get: function(){return fontSize;}, set: function(_){fontSize=_;}},
+        tickFormatMaxMin:  {get: function(){return tickFormatMaxMin;}, set: function(_){tickFormatMaxMin=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top !== undefined    ? _.top    : margin.top;
+            margin.right  = _.right !== undefined  ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left !== undefined   ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration=_;
+            renderWatch.reset(duration);
+        }},
+        scale: {get: function(){return scale;}, set: function(_){
+            scale = _;
+            axis.scale(scale);
+            isOrdinal = typeof scale.rangeBands === 'function';
+            nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+    nv.utils.inheritOptionsD3(chart, axis, ['orient', 'tickValues', 'tickSubdivide', 'tickSize', 'tickPadding', 'tickFormat']);
+    nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']);
+
+    return chart;
+};
+nv.models.boxPlot = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0},
+        width = 730,
+        height = 300,
+        id = Math.floor(Math.random() * 10000), // Create semi-unique ID in case user doesn't select one
+        xScale = d3.scale.ordinal(),
+        yScale = d3.scale.linear(),
+        getX  = function(d) { return d.label }, // Default data model selectors.
+        getQ1 = function(d) { return d.values.Q1 },
+        getQ2 = function(d) { return d.values.Q2 },
+        getQ3 = function(d) { return d.values.Q3 },
+        getWl = function(d) { return d.values.whisker_low },
+        getWh = function(d) { return d.values.whisker_high },
+        getColor = function(d) { return d.color },
+        getOlItems  = function(d) { return d.values.outliers },
+        getOlValue = function(d, i, j) { return d },
+        getOlLabel = function(d, i, j) { return d },
+        getOlColor = function(d, i, j) { return undefined },
+        color = nv.utils.defaultColor(),
+        container = null,
+        xDomain, xRange,
+        yDomain, yRange,
+        dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd'),
+        duration = 250,
+        maxBoxWidth = null;
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var xScale0, yScale0;
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        selection.each(function(data) {
+            var availableWidth = width - margin.left - margin.right,
+                availableHeight = height - margin.top - margin.bottom;
+
+            container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            // Setup Scales
+            xScale.domain(xDomain || data.map(function(d,i) { return getX(d,i); }))
+                .rangeBands(xRange || [0, availableWidth], 0.1);
+
+            // if we know yDomain, no need to calculate
+            var yData = []
+            if (!yDomain) {
+                // (y-range is based on quartiles, whiskers and outliers)
+                var values = [], yMin, yMax;
+                data.forEach(function (d, i) {
+                    var q1 = getQ1(d), q3 = getQ3(d), wl = getWl(d), wh = getWh(d);
+                    var olItems = getOlItems(d);
+                    if (olItems) {
+                        olItems.forEach(function (e, i) {
+                            values.push(getOlValue(e, i, undefined));
+                        });
+                    }
+                    if (wl) { values.push(wl) }
+                    if (q1) { values.push(q1) }
+                    if (q3) { values.push(q3) }
+                    if (wh) { values.push(wh) }
+                });
+                yMin = d3.min(values);
+                yMax = d3.max(values);
+                yData = [ yMin, yMax ] ;
+            }
+
+            yScale.domain(yDomain || yData);
+            yScale.range(yRange || [availableHeight, 0]);
+
+            //store old scales if they exist
+            xScale0 = xScale0 || xScale;
+            yScale0 = yScale0 || yScale.copy().range([yScale(0),yScale(0)]);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap').data([data]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap');
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            var boxplots = wrap.selectAll('.nv-boxplot').data(function(d) { return d });
+            var boxEnter = boxplots.enter().append('g').style('stroke-opacity', 1e-6).style('fill-opacity', 1e-6);
+            boxplots
+                .attr('class', 'nv-boxplot')
+                .attr('transform', function(d,i,j) { return 'translate(' + (xScale(getX(d,i)) + xScale.rangeBand() * 0.05) + ', 0)'; })
+                .classed('hover', function(d) { return d.hover });
+            boxplots
+                .watchTransition(renderWatch, 'nv-boxplot: boxplots')
+                .style('stroke-opacity', 1)
+                .style('fill-opacity', 0.75)
+                .delay(function(d,i) { return i * duration / data.length })
+                .attr('transform', function(d,i) {
+                    return 'translate(' + (xScale(getX(d,i)) + xScale.rangeBand() * 0.05) + ', 0)';
+                });
+            boxplots.exit().remove();
+
+            // ----- add the SVG elements for each boxPlot -----
+
+            // conditionally append whisker lines
+            boxEnter.each(function(d,i) {
+                var box = d3.select(this);
+                [getWl, getWh].forEach(function (f) {
+                    if (f(d) !== undefined && f(d) !== null) {
+                        var key = (f === getWl) ? 'low' : 'high';
+                        box.append('line')
+                          .style('stroke', getColor(d) || color(d,i))
+                          .attr('class', 'nv-boxplot-whisker nv-boxplot-' + key);
+                        box.append('line')
+                          .style('stroke', getColor(d) || color(d,i))
+                          .attr('class', 'nv-boxplot-tick nv-boxplot-' + key);
+                    }
+                });
+            });
+
+            var box_width = function() { return (maxBoxWidth === null ? xScale.rangeBand() * 0.9 : Math.min(75, xScale.rangeBand() * 0.9)); };
+            var box_left  = function() { return xScale.rangeBand() * 0.45 - box_width()/2; };
+            var box_right = function() { return xScale.rangeBand() * 0.45 + box_width()/2; };
+
+            // update whisker lines and ticks
+            [getWl, getWh].forEach(function (f) {
+                var key = (f === getWl) ? 'low' : 'high';
+                var endpoint = (f === getWl) ? getQ1 : getQ3;
+                boxplots.select('line.nv-boxplot-whisker.nv-boxplot-' + key)
+                  .watchTransition(renderWatch, 'nv-boxplot: boxplots')
+                    .attr('x1', xScale.rangeBand() * 0.45 )
+                    .attr('y1', function(d,i) { return yScale(f(d)); })
+                    .attr('x2', xScale.rangeBand() * 0.45 )
+                    .attr('y2', function(d,i) { return yScale(endpoint(d)); });
+                boxplots.select('line.nv-boxplot-tick.nv-boxplot-' + key)
+                  .watchTransition(renderWatch, 'nv-boxplot: boxplots')
+                    .attr('x1', box_left )
+                    .attr('y1', function(d,i) { return yScale(f(d)); })
+                    .attr('x2', box_right )
+                    .attr('y2', function(d,i) { return yScale(f(d)); });
+            });
+
+            [getWl, getWh].forEach(function (f) {
+                var key = (f === getWl) ? 'low' : 'high';
+                boxEnter.selectAll('.nv-boxplot-' + key)
+                  .on('mouseover', function(d,i,j) {
+                      d3.select(this).classed('hover', true);
+                      dispatch.elementMouseover({
+                          series: { key: f(d), color: getColor(d) || color(d,j) },
+                          e: d3.event
+                      });
+                  })
+                  .on('mouseout', function(d,i,j) {
+                      d3.select(this).classed('hover', false);
+                      dispatch.elementMouseout({
+                          series: { key: f(d), color: getColor(d) || color(d,j) },
+                          e: d3.event
+                      });
+                  })
+                  .on('mousemove', function(d,i) {
+                      dispatch.elementMousemove({e: d3.event});
+                  });
+            });
+
+            // boxes
+            boxEnter.append('rect')
+                .attr('class', 'nv-boxplot-box')
+                // tooltip events
+                .on('mouseover', function(d,i) {
+                    d3.select(this).classed('hover', true);
+                    dispatch.elementMouseover({
+                        key: getX(d),
+                        value: getX(d),
+                        series: [
+                            { key: 'Q3', value: getQ3(d), color: getColor(d) || color(d,i) },
+                            { key: 'Q2', value: getQ2(d), color: getColor(d) || color(d,i) },
+                            { key: 'Q1', value: getQ1(d), color: getColor(d) || color(d,i) }
+                        ],
+                        data: d,
+                        index: i,
+                        e: d3.event
+                    });
+                })
+                .on('mouseout', function(d,i) {
+                    d3.select(this).classed('hover', false);
+                    dispatch.elementMouseout({
+                        key: getX(d),
+                        value: getX(d),
+                        series: [
+                            { key: 'Q3', value: getQ3(d), color: getColor(d) || color(d,i) },
+                            { key: 'Q2', value: getQ2(d), color: getColor(d) || color(d,i) },
+                            { key: 'Q1', value: getQ1(d), color: getColor(d) || color(d,i) }
+                        ],
+                        data: d,
+                        index: i,
+                        e: d3.event
+                    });
+                })
+                .on('mousemove', function(d,i) {
+                    dispatch.elementMousemove({e: d3.event});
+                });
+
+            // box transitions
+            boxplots.select('rect.nv-boxplot-box')
+              .watchTransition(renderWatch, 'nv-boxplot: boxes')
+                .attr('y', function(d,i) { return yScale(getQ3(d)); })
+                .attr('width', box_width)
+                .attr('x', box_left )
+                .attr('height', function(d,i) { return Math.abs(yScale(getQ3(d)) - yScale(getQ1(d))) || 1 })
+                .style('fill', function(d,i) { return getColor(d) || color(d,i) })
+                .style('stroke', function(d,i) { return getColor(d) || color(d,i) });
+
+            // median line
+            boxEnter.append('line').attr('class', 'nv-boxplot-median');
+
+            boxplots.select('line.nv-boxplot-median')
+              .watchTransition(renderWatch, 'nv-boxplot: boxplots line')
+                .attr('x1', box_left)
+                .attr('y1', function(d,i) { return yScale(getQ2(d)); })
+                .attr('x2', box_right)
+                .attr('y2', function(d,i) { return yScale(getQ2(d)); });
+
+            // outliers
+            var outliers = boxplots.selectAll('.nv-boxplot-outlier').data(function(d) {
+                return getOlItems(d) || [];
+            });
+            outliers.enter().append('circle')
+                .style('fill', function(d,i,j) { return getOlColor(d,i,j) || color(d,j) })
+                .style('stroke', function(d,i,j) { return getOlColor(d,i,j) || color(d,j) })
+                .style('z-index', 9000)
+                .on('mouseover', function(d,i,j) {
+                    d3.select(this).classed('hover', true);
+                    dispatch.elementMouseover({
+                        series: { key: getOlLabel(d,i,j), color: getOlColor(d,i,j) || color(d,j) },
+                        e: d3.event
+                    });
+                })
+                .on('mouseout', function(d,i,j) {
+                    d3.select(this).classed('hover', false);
+                    dispatch.elementMouseout({
+                        series: { key: getOlLabel(d,i,j), color: getOlColor(d,i,j) || color(d,j) },
+                        e: d3.event
+                    });
+                })
+                .on('mousemove', function(d,i) {
+                    dispatch.elementMousemove({e: d3.event});
+                });
+            outliers.attr('class', 'nv-boxplot-outlier');
+            outliers
+              .watchTransition(renderWatch, 'nv-boxplot: nv-boxplot-outlier')
+                .attr('cx', xScale.rangeBand() * 0.45)
+                .attr('cy', function(d,i,j) { return yScale(getOlValue(d,i,j)); })
+                .attr('r', '3');
+            outliers.exit().remove();
+
+            //store old scales for use in transitions on update
+            xScale0 = xScale.copy();
+            yScale0 = yScale.copy();
+        });
+
+        renderWatch.renderEnd('nv-boxplot immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:       {get: function(){return width;}, set: function(_){width=_;}},
+        height:      {get: function(){return height;}, set: function(_){height=_;}},
+        maxBoxWidth: {get: function(){return maxBoxWidth;}, set: function(_){maxBoxWidth=_;}},
+        x:           {get: function(){return getX;}, set: function(_){getX=_;}},
+        q1: {get: function(){return getQ1;}, set: function(_){getQ1=_;}},
+        q2: {get: function(){return getQ2;}, set: function(_){getQ2=_;}},
+        q3: {get: function(){return getQ3;}, set: function(_){getQ3=_;}},
+        wl: {get: function(){return getWl;}, set: function(_){getWl=_;}},
+        wh: {get: function(){return getWh;}, set: function(_){getWh=_;}},
+        itemColor:    {get: function(){return getColor;}, set: function(_){getColor=_;}},
+        outliers:     {get: function(){return getOlItems;}, set: function(_){getOlItems=_;}},
+        outlierValue: {get: function(){return getOlValue;}, set: function(_){getOlValue=_;}},
+        outlierLabel: {get: function(){return getOlLabel;}, set: function(_){getOlLabel=_;}},
+        outlierColor: {get: function(){return getOlColor;}, set: function(_){getOlColor=_;}},
+        xScale:  {get: function(){return xScale;}, set: function(_){xScale=_;}},
+        yScale:  {get: function(){return yScale;}, set: function(_){yScale=_;}},
+        xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+        yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+        xRange:  {get: function(){return xRange;}, set: function(_){xRange=_;}},
+        yRange:  {get: function(){return yRange;}, set: function(_){yRange=_;}},
+        id:          {get: function(){return id;}, set: function(_){id=_;}},
+        // rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}},
+        y: {
+            get: function() {
+                console.warn('BoxPlot \'y\' chart option is deprecated. Please use model overrides instead.');
+                return {};
+            },
+            set: function(_) {
+                console.warn('BoxPlot \'y\' chart option is deprecated. Please use model overrides instead.');
+            }
+        },
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+nv.models.boxPlotChart = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var boxplot = nv.models.boxPlot(),
+        xAxis = nv.models.axis(),
+        yAxis = nv.models.axis();
+
+    var margin = {top: 15, right: 10, bottom: 50, left: 60},
+        width = null,
+        height = null,
+        color = nv.utils.getColor(),
+        showXAxis = true,
+        showYAxis = true,
+        rightAlignYAxis = false,
+        staggerLabels = false,
+        tooltip = nv.models.tooltip(),
+        x, y,
+        noData = 'No Data Available.',
+        dispatch = d3.dispatch('beforeUpdate', 'renderEnd'),
+        duration = 250;
+
+    xAxis
+        .orient('bottom')
+        .showMaxMin(false)
+        .tickFormat(function(d) { return d })
+    ;
+    yAxis
+        .orient((rightAlignYAxis) ? 'right' : 'left')
+        .tickFormat(d3.format(',.1f'))
+    ;
+
+    tooltip.duration(0);
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        renderWatch.models(boxplot);
+        if (showXAxis) renderWatch.models(xAxis);
+        if (showYAxis) renderWatch.models(yAxis);
+
+        selection.each(function(data) {
+            var container = d3.select(this), that = this;
+            nv.utils.initSVG(container);
+            var availableWidth = (width  || parseInt(container.style('width')) || 960) - margin.left - margin.right;
+            var availableHeight = (height || parseInt(container.style('height')) || 400) - margin.top - margin.bottom;
+
+            chart.update = function() {
+                dispatch.beforeUpdate();
+                container.transition().duration(duration).call(chart);
+            };
+            chart.container = this;
+
+            // TODO still need to find a way to validate quartile data presence using boxPlot callbacks.
+            // Display No Data message if there's nothing to show. (quartiles required at minimum).
+            if (!data || !data.length) {
+                var noDataText = container.selectAll('.nv-noData').data([noData]);
+
+                noDataText.enter().append('text')
+                    .attr('class', 'nvd3 nv-noData')
+                    .attr('dy', '-.7em')
+                    .style('text-anchor', 'middle');
+
+                noDataText
+                    .attr('x', margin.left + availableWidth / 2)
+                    .attr('y', margin.top + availableHeight / 2)
+                    .text(function(d) { return d });
+
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            // Setup Scales
+            x = boxplot.xScale();
+            y = boxplot.yScale().clamp(true);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-boxPlotWithAxes').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-boxPlotWithAxes').append('g');
+            var defsEnter = gEnter.append('defs');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-x nv-axis');
+            gEnter.append('g').attr('class', 'nv-y nv-axis')
+                .append('g').attr('class', 'nv-zeroLine')
+                .append('line');
+
+            gEnter.append('g').attr('class', 'nv-barsWrap');
+            g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            if (rightAlignYAxis) {
+                g.select('.nv-y.nv-axis')
+                    .attr('transform', 'translate(' + availableWidth + ',0)');
+            }
+
+            // Main Chart Component(s)
+            boxplot.width(availableWidth).height(availableHeight);
+
+            var barsWrap = g.select('.nv-barsWrap')
+                .datum(data.filter(function(d) { return !d.disabled }))
+
+            barsWrap.transition().call(boxplot);
+
+            defsEnter.append('clipPath')
+                .attr('id', 'nv-x-label-clip-' + boxplot.id())
+                .append('rect');
+
+            g.select('#nv-x-label-clip-' + boxplot.id() + ' rect')
+                .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1))
+                .attr('height', 16)
+                .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 ));
+
+            // Setup Axes
+            if (showXAxis) {
+                xAxis
+                    .scale(x)
+                    .ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+                    .tickSize(-availableHeight, 0);
+
+                g.select('.nv-x.nv-axis').attr('transform', 'translate(0,' + y.range()[0] + ')');
+                g.select('.nv-x.nv-axis').call(xAxis);
+
+                var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
+                if (staggerLabels) {
+                    xTicks
+                        .selectAll('text')
+                        .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 === 0 ? '5' : '17') + ')' })
+                }
+            }
+
+            if (showYAxis) {
+                yAxis
+                    .scale(y)
+                    .ticks( Math.floor(availableHeight/36) ) // can't use nv.utils.calcTicksY with Object data
+                    .tickSize( -availableWidth, 0);
+
+                g.select('.nv-y.nv-axis').call(yAxis);
+            }
+
+            // Zero line
+            g.select('.nv-zeroLine line')
+                .attr('x1',0)
+                .attr('x2',availableWidth)
+                .attr('y1', y(0))
+                .attr('y2', y(0))
+            ;
+
+            //============================================================
+            // Event Handling/Dispatching (in chart's scope)
+            //------------------------------------------------------------
+        });
+
+        renderWatch.renderEnd('nv-boxplot chart immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    boxplot.dispatch.on('elementMouseover.tooltip', function(evt) {
+        tooltip.data(evt).hidden(false);
+    });
+
+    boxplot.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.data(evt).hidden(true);
+    });
+
+    boxplot.dispatch.on('elementMousemove.tooltip', function(evt) {
+        tooltip();
+    });
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.boxplot = boxplot;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+        staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+        showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        tooltipContent:    {get: function(){return tooltip;}, set: function(_){tooltip=_;}},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+            boxplot.duration(duration);
+            xAxis.duration(duration);
+            yAxis.duration(duration);
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+            boxplot.color(color);
+        }},
+        rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+            rightAlignYAxis = _;
+            yAxis.orient( (_) ? 'right' : 'left');
+        }}
+    });
+
+    nv.utils.inheritOptions(chart, boxplot);
+    nv.utils.initOptions(chart);
+
+    return chart;
+}
+
+// Chart design based on the recommendations of Stephen Few. Implementation
+// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
+// http://projects.instantcognition.com/protovis/bulletchart/
+
+nv.models.bullet = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , orient = 'left' // TODO top & bottom
+        , reverse = false
+        , ranges = function(d) { return d.ranges }
+        , markers = function(d) { return d.markers ? d.markers : [] }
+        , markerLines = function(d) { return d.markerLines ? d.markerLines : [0] }
+        , measures = function(d) { return d.measures }
+        , rangeLabels = function(d) { return d.rangeLabels ? d.rangeLabels : [] }
+        , markerLabels = function(d) { return d.markerLabels ? d.markerLabels : []  }
+        , markerLineLabels = function(d) { return d.markerLineLabels ? d.markerLineLabels : []  }
+        , measureLabels = function(d) { return d.measureLabels ? d.measureLabels : []  }
+        , forceX = [0] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.)
+        , width = 380
+        , height = 30
+        , container = null
+        , tickFormat = null
+        , color = nv.utils.getColor(['#1f77b4'])
+        , dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove')
+        , defaultRangeLabels = ["Maximum", "Mean", "Minimum"]
+        , legacyRangeClassNames = ["Max", "Avg", "Min"]
+        , duration = 1000
+        ;
+
+    function sortLabels(labels, values){
+        var lz = labels.slice();
+        labels.sort(function(a, b){
+            var iA = lz.indexOf(a);
+            var iB = lz.indexOf(b);
+            return d3.descending(values[iA], values[iB]);
+        });
+    };
+
+    function chart(selection) {
+        selection.each(function(d, i) {
+            var availableWidth = width - margin.left - margin.right,
+                availableHeight = height - margin.top - margin.bottom;
+
+            container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            var rangez = ranges.call(this, d, i).slice(),
+                markerz = markers.call(this, d, i).slice(),
+                markerLinez = markerLines.call(this, d, i).slice(),
+                measurez = measures.call(this, d, i).slice(),
+                rangeLabelz = rangeLabels.call(this, d, i).slice(),
+                markerLabelz = markerLabels.call(this, d, i).slice(),
+                markerLineLabelz = markerLineLabels.call(this, d, i).slice(),
+                measureLabelz = measureLabels.call(this, d, i).slice();
+
+            // Sort labels according to their sorted values
+            sortLabels(rangeLabelz, rangez);
+            sortLabels(markerLabelz, markerz);
+            sortLabels(markerLineLabelz, markerLinez);
+            sortLabels(measureLabelz, measurez);
+
+            // sort values descending
+            rangez.sort(d3.descending);
+            markerz.sort(d3.descending);
+            markerLinez.sort(d3.descending);
+            measurez.sort(d3.descending);
+
+            // Setup Scales
+            // Compute the new x-scale.
+            var x1 = d3.scale.linear()
+                .domain( d3.extent(d3.merge([forceX, rangez])) )
+                .range(reverse ? [availableWidth, 0] : [0, availableWidth]);
+
+            // Retrieve the old x-scale, if this is an update.
+            var x0 = this.__chart__ || d3.scale.linear()
+                .domain([0, Infinity])
+                .range(x1.range());
+
+            // Stash the new scale.
+            this.__chart__ = x1;
+
+            var rangeMin = d3.min(rangez), //rangez[2]
+                rangeMax = d3.max(rangez), //rangez[0]
+                rangeAvg = rangez[1];
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-bullet').data([d]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bullet');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            for(var i=0,il=rangez.length; i<il; i++){
+                var rangeClassNames = 'nv-range nv-range'+i;
+                if(i <= 2){
+                    rangeClassNames = rangeClassNames + ' nv-range'+legacyRangeClassNames[i];
+                }
+                gEnter.append('rect').attr('class', rangeClassNames);
+            }
+
+            gEnter.append('rect').attr('class', 'nv-measure');
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0)
+                w1 = function(d) { return Math.abs(x1(d) - x1(0)) };
+            var xp0 = function(d) { return d < 0 ? x0(d) : x0(0) },
+                xp1 = function(d) { return d < 0 ? x1(d) : x1(0) };
+
+            for(var i=0,il=rangez.length; i<il; i++){
+                var range = rangez[i];
+                g.select('rect.nv-range'+i)
+                    .datum(range)
+                    .attr('height', availableHeight)
+                    .transition()
+                    .duration(duration)
+                    .attr('width', w1(range))
+                    .attr('x', xp1(range))
+            }
+
+            g.select('rect.nv-measure')
+                .style('fill', color)
+                .attr('height', availableHeight / 3)
+                .attr('y', availableHeight / 3)
+                .on('mouseover', function() {
+                    dispatch.elementMouseover({
+                        value: measurez[0],
+                        label: measureLabelz[0] || 'Current',
+                        color: d3.select(this).style("fill")
+                    })
+                })
+                .on('mousemove', function() {
+                    dispatch.elementMousemove({
+                        value: measurez[0],
+                        label: measureLabelz[0] || 'Current',
+                        color: d3.select(this).style("fill")
+                    })
+                })
+                .on('mouseout', function() {
+                    dispatch.elementMouseout({
+                        value: measurez[0],
+                        label: measureLabelz[0] || 'Current',
+                        color: d3.select(this).style("fill")
+                    })
+                })
+                .transition()
+                .duration(duration)
+                .attr('width', measurez < 0 ?
+                    x1(0) - x1(measurez[0])
+                    : x1(measurez[0]) - x1(0))
+                .attr('x', xp1(measurez));
+
+            var h3 =  availableHeight / 6;
+
+            var markerData = markerz.map( function(marker, index) {
+                return {value: marker, label: markerLabelz[index]}
+            });
+            gEnter
+              .selectAll("path.nv-markerTriangle")
+              .data(markerData)
+              .enter()
+              .append('path')
+              .attr('class', 'nv-markerTriangle')
+              .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z')
+              .on('mouseover', function(d) {
+                dispatch.elementMouseover({
+                  value: d.value,
+                  label: d.label || 'Previous',
+                  color: d3.select(this).style("fill"),
+                  pos: [x1(d.value), availableHeight/2]
+                })
+
+              })
+              .on('mousemove', function(d) {
+                  dispatch.elementMousemove({
+                      value: d.value,
+                      label: d.label || 'Previous',
+                      color: d3.select(this).style("fill")
+                  })
+              })
+              .on('mouseout', function(d, i) {
+                  dispatch.elementMouseout({
+                      value: d.value,
+                      label: d.label || 'Previous',
+                      color: d3.select(this).style("fill")
+                  })
+              });
+
+            g.selectAll("path.nv-markerTriangle")
+              .data(markerData)
+              .transition()
+              .duration(duration)
+              .attr('transform', function(d) { return 'translate(' + x1(d.value) + ',' + (availableHeight / 2) + ')' });
+
+            var markerLinesData = markerLinez.map( function(marker, index) {
+                return {value: marker, label: markerLineLabelz[index]}
+            });
+            gEnter
+              .selectAll("line.nv-markerLine")
+              .data(markerLinesData)
+              .enter()
+              .append('line')
+              .attr('cursor', '')
+              .attr('class', 'nv-markerLine')
+              .attr('x1', function(d) { return x1(d.value) })
+              .attr('y1', '2')
+              .attr('x2', function(d) { return x1(d.value) })
+              .attr('y2', availableHeight - 2)
+              .on('mouseover', function(d) {
+                dispatch.elementMouseover({
+                  value: d.value,
+                  label: d.label || 'Previous',
+                  color: d3.select(this).style("fill"),
+                  pos: [x1(d.value), availableHeight/2]
+                })
+
+              })
+              .on('mousemove', function(d) {
+                  dispatch.elementMousemove({
+                      value: d.value,
+                      label: d.label || 'Previous',
+                      color: d3.select(this).style("fill")
+                  })
+              })
+              .on('mouseout', function(d, i) {
+                  dispatch.elementMouseout({
+                      value: d.value,
+                      label: d.label || 'Previous',
+                      color: d3.select(this).style("fill")
+                  })
+              });
+
+            g.selectAll("line.nv-markerLine")
+              .data(markerLinesData)
+              .transition()
+              .duration(duration)
+              .attr('x1', function(d) { return x1(d.value) })
+              .attr('x2', function(d) { return x1(d.value) });
+
+            wrap.selectAll('.nv-range')
+                .on('mouseover', function(d,i) {
+                    var label = rangeLabelz[i] || defaultRangeLabels[i];
+                    dispatch.elementMouseover({
+                        value: d,
+                        label: label,
+                        color: d3.select(this).style("fill")
+                    })
+                })
+                .on('mousemove', function() {
+                    dispatch.elementMousemove({
+                        value: measurez[0],
+                        label: measureLabelz[0] || 'Previous',
+                        color: d3.select(this).style("fill")
+                    })
+                })
+                .on('mouseout', function(d,i) {
+                    var label = rangeLabelz[i] || defaultRangeLabels[i];
+                    dispatch.elementMouseout({
+                        value: d,
+                        label: label,
+                        color: d3.select(this).style("fill")
+                    })
+                });
+        });
+
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        ranges:      {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good)
+        markers:     {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal)
+        measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast)
+        forceX:      {get: function(){return forceX;}, set: function(_){forceX=_;}},
+        width:    {get: function(){return width;}, set: function(_){width=_;}},
+        height:    {get: function(){return height;}, set: function(_){height=_;}},
+        tickFormat:    {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}},
+        duration:    {get: function(){return duration;}, set: function(_){duration=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom
+            orient = _;
+            reverse = orient == 'right' || orient == 'bottom';
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+    return chart;
+};
+
+
+
+// Chart design based on the recommendations of Stephen Few. Implementation
+// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
+// http://projects.instantcognition.com/protovis/bulletchart/
+nv.models.bulletChart = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var bullet = nv.models.bullet();
+    var tooltip = nv.models.tooltip();
+
+    var orient = 'left' // TODO top & bottom
+        , reverse = false
+        , margin = {top: 5, right: 40, bottom: 20, left: 120}
+        , ranges = function(d) { return d.ranges }
+        , markers = function(d) { return d.markers ? d.markers : [] }
+        , measures = function(d) { return d.measures }
+        , width = null
+        , height = 55
+        , tickFormat = null
+        , ticks = null
+        , noData = null
+        , dispatch = d3.dispatch()
+        ;
+
+    tooltip
+        .duration(0)
+        .headerEnabled(false);
+
+    function chart(selection) {
+        selection.each(function(d, i) {
+            var container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = height - margin.top - margin.bottom,
+                that = this;
+
+            chart.update = function() { chart(selection) };
+            chart.container = this;
+
+            // Display No Data message if there's nothing to show.
+            if (!d || !ranges.call(this, d, i)) {
+                nv.utils.noData(chart, container)
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
+                markerz = markers.call(this, d, i).slice().sort(d3.descending),
+                measurez = measures.call(this, d, i).slice().sort(d3.descending);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-bulletChart').data([d]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bulletChart');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-bulletWrap');
+            gEnter.append('g').attr('class', 'nv-titles');
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            // Compute the new x-scale.
+            var x1 = d3.scale.linear()
+                .domain([0, Math.max(rangez[0], (markerz[0] || 0), measurez[0])])  // TODO: need to allow forceX and forceY, and xDomain, yDomain
+                .range(reverse ? [availableWidth, 0] : [0, availableWidth]);
+
+            // Retrieve the old x-scale, if this is an update.
+            var x0 = this.__chart__ || d3.scale.linear()
+                .domain([0, Infinity])
+                .range(x1.range());
+
+            // Stash the new scale.
+            this.__chart__ = x1;
+
+            var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0)
+                w1 = function(d) { return Math.abs(x1(d) - x1(0)) };
+
+            var title = gEnter.select('.nv-titles').append('g')
+                .attr('text-anchor', 'end')
+                .attr('transform', 'translate(-6,' + (height - margin.top - margin.bottom) / 2 + ')');
+            title.append('text')
+                .attr('class', 'nv-title')
+                .text(function(d) { return d.title; });
+
+            title.append('text')
+                .attr('class', 'nv-subtitle')
+                .attr('dy', '1em')
+                .text(function(d) { return d.subtitle; });
+
+            bullet
+                .width(availableWidth)
+                .height(availableHeight);
+
+            var bulletWrap = g.select('.nv-bulletWrap');
+            d3.transition(bulletWrap).call(bullet);
+
+            // Compute the tick format.
+            var format = tickFormat || x1.tickFormat( availableWidth / 100 );
+
+            // Update the tick groups.
+            var tick = g.selectAll('g.nv-tick')
+                .data(x1.ticks( ticks ? ticks : (availableWidth / 50) ), function(d) {
+                    return this.textContent || format(d);
+                });
+
+            // Initialize the ticks with the old scale, x0.
+            var tickEnter = tick.enter().append('g')
+                .attr('class', 'nv-tick')
+                .attr('transform', function(d) { return 'translate(' + x0(d) + ',0)' })
+                .style('opacity', 1e-6);
+
+            tickEnter.append('line')
+                .attr('y1', availableHeight)
+                .attr('y2', availableHeight * 7 / 6);
+
+            tickEnter.append('text')
+                .attr('text-anchor', 'middle')
+                .attr('dy', '1em')
+                .attr('y', availableHeight * 7 / 6)
+                .text(format);
+
+            // Transition the updating ticks to the new scale, x1.
+            var tickUpdate = d3.transition(tick)
+                .transition()
+                .duration(bullet.duration())
+                .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' })
+                .style('opacity', 1);
+
+            tickUpdate.select('line')
+                .attr('y1', availableHeight)
+                .attr('y2', availableHeight * 7 / 6);
+
+            tickUpdate.select('text')
+                .attr('y', availableHeight * 7 / 6);
+
+            // Transition the exiting ticks to the new scale, x1.
+            d3.transition(tick.exit())
+                .transition()
+                .duration(bullet.duration())
+                .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' })
+                .style('opacity', 1e-6)
+                .remove();
+        });
+
+        d3.timer.flush();
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    bullet.dispatch.on('elementMouseover.tooltip', function(evt) {
+        evt['series'] = {
+            key: evt.label,
+            value: evt.value,
+            color: evt.color
+        };
+        tooltip.data(evt).hidden(false);
+    });
+
+    bullet.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.hidden(true);
+    });
+
+    bullet.dispatch.on('elementMousemove.tooltip', function(evt) {
+        tooltip();
+    });
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.bullet = bullet;
+    chart.dispatch = dispatch;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        ranges:      {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good)
+        markers:     {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal)
+        measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast)
+        width:    {get: function(){return width;}, set: function(_){width=_;}},
+        height:    {get: function(){return height;}, set: function(_){height=_;}},
+        tickFormat:    {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}},
+        ticks:    {get: function(){return ticks;}, set: function(_){ticks=_;}},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom
+            orient = _;
+            reverse = orient == 'right' || orient == 'bottom';
+        }}
+    });
+
+    nv.utils.inheritOptions(chart, bullet);
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+
+
+
+nv.models.candlestickBar = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , width = null
+        , height = null
+        , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+        , container
+        , x = d3.scale.linear()
+        , y = d3.scale.linear()
+        , getX = function(d) { return d.x }
+        , getY = function(d) { return d.y }
+        , getOpen = function(d) { return d.open }
+        , getClose = function(d) { return d.close }
+        , getHigh = function(d) { return d.high }
+        , getLow = function(d) { return d.low }
+        , forceX = []
+        , forceY = []
+        , padData     = false // If true, adds half a data points width to front and back, for lining up a line chart with a bar chart
+        , clipEdge = true
+        , color = nv.utils.defaultColor()
+        , interactive = false
+        , xDomain
+        , yDomain
+        , xRange
+        , yRange
+        , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd', 'chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove')
+        ;
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    function chart(selection) {
+        selection.each(function(data) {
+            container = d3.select(this);
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = nv.utils.availableHeight(height, container, margin);
+
+            nv.utils.initSVG(container);
+
+            // Width of the candlestick bars.
+            var barWidth = (availableWidth / data[0].values.length) * .45;
+
+            // Setup Scales
+            x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) ));
+
+            if (padData)
+                x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5)  / data[0].values.length ]);
+            else
+                x.range(xRange || [5 + barWidth / 2, availableWidth - barWidth / 2 - 5]);
+
+            y.domain(yDomain || [
+                    d3.min(data[0].values.map(getLow).concat(forceY)),
+                    d3.max(data[0].values.map(getHigh).concat(forceY))
+                ]
+            ).range(yRange || [availableHeight, 0]);
+
+            // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+            if (x.domain()[0] === x.domain()[1])
+                x.domain()[0] ?
+                    x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+                    : x.domain([-1,1]);
+
+            if (y.domain()[0] === y.domain()[1])
+                y.domain()[0] ?
+                    y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
+                    : y.domain([-1,1]);
+
+            // Setup containers and skeleton of chart
+            var wrap = d3.select(this).selectAll('g.nv-wrap.nv-candlestickBar').data([data[0].values]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-candlestickBar');
+            var defsEnter = wrapEnter.append('defs');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-ticks');
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            container
+                .on('click', function(d,i) {
+                    dispatch.chartClick({
+                        data: d,
+                        index: i,
+                        pos: d3.event,
+                        id: id
+                    });
+                });
+
+            defsEnter.append('clipPath')
+                .attr('id', 'nv-chart-clip-path-' + id)
+                .append('rect');
+
+            wrap.select('#nv-chart-clip-path-' + id + ' rect')
+                .attr('width', availableWidth)
+                .attr('height', availableHeight);
+
+            g   .attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : '');
+
+            var ticks = wrap.select('.nv-ticks').selectAll('.nv-tick')
+                .data(function(d) { return d });
+            ticks.exit().remove();
+
+            var tickGroups = ticks.enter().append('g');
+
+            // The colors are currently controlled by CSS.
+            ticks
+                .attr('class', function(d, i, j) { return (getOpen(d, i) > getClose(d, i) ? 'nv-tick negative' : 'nv-tick positive') + ' nv-tick-' + j + '-' + i});
+
+            var lines = tickGroups.append('line')
+                .attr('class', 'nv-candlestick-lines')
+                .attr('transform', function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)'; })
+                .attr('x1', 0)
+                .attr('y1', function(d, i) { return y(getHigh(d, i)); })
+                .attr('x2', 0)
+                .attr('y2', function(d, i) { return y(getLow(d, i)); });
+
+            var rects = tickGroups.append('rect')
+                .attr('class', 'nv-candlestick-rects nv-bars')
+                .attr('transform', function(d, i) {
+                    return 'translate(' + (x(getX(d, i)) - barWidth/2) + ','
+                    + (y(getY(d, i)) - (getOpen(d, i) > getClose(d, i) ? (y(getClose(d, i)) - y(getOpen(d, i))) : 0))
+                    + ')';
+                })
+                .attr('x', 0)
+                .attr('y', 0)
+                .attr('width', barWidth)
+                .attr('height', function(d, i) {
+                    var open = getOpen(d, i);
+                    var close = getClose(d, i);
+                    return open > close ? y(close) - y(open) : y(open) - y(close);
+                });
+
+            ticks.select('.nv-candlestick-lines').transition()
+                .attr('transform', function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)'; })
+                .attr('x1', 0)
+                .attr('y1', function(d, i) { return y(getHigh(d, i)); })
+                .attr('x2', 0)
+                .attr('y2', function(d, i) { return y(getLow(d, i)); });
+
+            ticks.select('.nv-candlestick-rects').transition()
+                .attr('transform', function(d, i) {
+                    return 'translate(' + (x(getX(d, i)) - barWidth/2) + ','
+                    + (y(getY(d, i)) - (getOpen(d, i) > getClose(d, i) ? (y(getClose(d, i)) - y(getOpen(d, i))) : 0))
+                    + ')';
+                })
+                .attr('x', 0)
+                .attr('y', 0)
+                .attr('width', barWidth)
+                .attr('height', function(d, i) {
+                    var open = getOpen(d, i);
+                    var close = getClose(d, i);
+                    return open > close ? y(close) - y(open) : y(open) - y(close);
+                });
+        });
+
+        return chart;
+    }
+
+
+    //Create methods to allow outside functions to highlight a specific bar.
+    chart.highlightPoint = function(pointIndex, isHoverOver) {
+        chart.clearHighlights();
+        container.select(".nv-candlestickBar .nv-tick-0-" + pointIndex)
+            .classed("hover", isHoverOver)
+        ;
+    };
+
+    chart.clearHighlights = function() {
+        container.select(".nv-candlestickBar .nv-tick.hover")
+            .classed("hover", false)
+        ;
+    };
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:    {get: function(){return width;}, set: function(_){width=_;}},
+        height:   {get: function(){return height;}, set: function(_){height=_;}},
+        xScale:   {get: function(){return x;}, set: function(_){x=_;}},
+        yScale:   {get: function(){return y;}, set: function(_){y=_;}},
+        xDomain:  {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+        yDomain:  {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+        xRange:   {get: function(){return xRange;}, set: function(_){xRange=_;}},
+        yRange:   {get: function(){return yRange;}, set: function(_){yRange=_;}},
+        forceX:   {get: function(){return forceX;}, set: function(_){forceX=_;}},
+        forceY:   {get: function(){return forceY;}, set: function(_){forceY=_;}},
+        padData:  {get: function(){return padData;}, set: function(_){padData=_;}},
+        clipEdge: {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+        id:       {get: function(){return id;}, set: function(_){id=_;}},
+        interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
+
+        x:     {get: function(){return getX;}, set: function(_){getX=_;}},
+        y:     {get: function(){return getY;}, set: function(_){getY=_;}},
+        open:  {get: function(){return getOpen();}, set: function(_){getOpen=_;}},
+        close: {get: function(){return getClose();}, set: function(_){getClose=_;}},
+        high:  {get: function(){return getHigh;}, set: function(_){getHigh=_;}},
+        low:   {get: function(){return getLow;}, set: function(_){getLow=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    != undefined ? _.top    : margin.top;
+            margin.right  = _.right  != undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   != undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+    return chart;
+};
+
+nv.models.cumulativeLineChart = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var lines = nv.models.line()
+        , xAxis = nv.models.axis()
+        , yAxis = nv.models.axis()
+        , legend = nv.models.legend()
+        , controls = nv.models.legend()
+        , interactiveLayer = nv.interactiveGuideline()
+        , tooltip = nv.models.tooltip()
+        ;
+
+    var margin = {top: 30, right: 30, bottom: 50, left: 60}
+        , marginTop = null
+        , color = nv.utils.defaultColor()
+        , width = null
+        , height = null
+        , showLegend = true
+        , showXAxis = true
+        , showYAxis = true
+        , rightAlignYAxis = false
+        , showControls = true
+        , useInteractiveGuideline = false
+        , rescaleY = true
+        , x //can be accessed via chart.xScale()
+        , y //can be accessed via chart.yScale()
+        , id = lines.id()
+        , state = nv.utils.state()
+        , defaultState = null
+        , noData = null
+        , average = function(d) { return d.average }
+        , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd')
+        , transitionDuration = 250
+        , duration = 250
+        , noErrorCheck = false  //if set to TRUE, will bypass an error check in the indexify function.
+        ;
+
+    state.index = 0;
+    state.rescaleY = rescaleY;
+
+    xAxis.orient('bottom').tickPadding(7);
+    yAxis.orient((rightAlignYAxis) ? 'right' : 'left');
+
+    tooltip.valueFormatter(function(d, i) {
+        return yAxis.tickFormat()(d, i);
+    }).headerFormatter(function(d, i) {
+        return xAxis.tickFormat()(d, i);
+    });
+
+    controls.updateState(false);
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var dx = d3.scale.linear()
+        , index = {i: 0, x: 0}
+        , renderWatch = nv.utils.renderWatch(dispatch, duration)
+        , currentYDomain
+        ;
+
+    var stateGetter = function(data) {
+        return function(){
+            return {
+                active: data.map(function(d) { return !d.disabled }),
+                index: index.i,
+                rescaleY: rescaleY
+            };
+        }
+    };
+
+    var stateSetter = function(data) {
+        return function(state) {
+            if (state.index !== undefined)
+                index.i = state.index;
+            if (state.rescaleY !== undefined)
+                rescaleY = state.rescaleY;
+            if (state.active !== undefined)
+                data.forEach(function(series,i) {
+                    series.disabled = !state.active[i];
+                });
+        }
+    };
+
+    function chart(selection) {
+        renderWatch.reset();
+        renderWatch.models(lines);
+        if (showXAxis) renderWatch.models(xAxis);
+        if (showYAxis) renderWatch.models(yAxis);
+        selection.each(function(data) {
+            var container = d3.select(this);
+            nv.utils.initSVG(container);
+            container.classed('nv-chart-' + id, true);
+            var that = this;
+
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = nv.utils.availableHeight(height, container, margin);
+
+            chart.update = function() {
+                if (duration === 0)
+                    container.call(chart);
+                else
+                    container.transition().duration(duration).call(chart)
+            };
+            chart.container = this;
+
+            state
+                .setter(stateSetter(data), chart.update)
+                .getter(stateGetter(data))
+                .update();
+
+            // DEPRECATED set state.disableddisabled
+            state.disabled = data.map(function(d) { return !!d.disabled });
+
+            if (!defaultState) {
+                var key;
+                defaultState = {};
+                for (key in state) {
+                    if (state[key] instanceof Array)
+                        defaultState[key] = state[key].slice(0);
+                    else
+                        defaultState[key] = state[key];
+                }
+            }
+
+            var indexDrag = d3.behavior.drag()
+                .on('dragstart', dragStart)
+                .on('drag', dragMove)
+                .on('dragend', dragEnd);
+
+
+            function dragStart(d,i) {
+                d3.select(chart.container)
+                    .style('cursor', 'ew-resize');
+            }
+
+            function dragMove(d,i) {
+                index.x = d3.event.x;
+                index.i = Math.round(dx.invert(index.x));
+                updateZero();
+            }
+
+            function dragEnd(d,i) {
+                d3.select(chart.container)
+                    .style('cursor', 'auto');
+
+                // update state and send stateChange with new index
+                state.index = index.i;
+                dispatch.stateChange(state);
+            }
+
+            // Display No Data message if there's nothing to show.
+            if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+                nv.utils.noData(chart, container)
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            // Setup Scales
+            x = lines.xScale();
+            y = lines.yScale();
+
+
+            dx.domain([0, data[0].values.length - 1]) //Assumes all series have same length
+                .range([0, availableWidth])
+                .clamp(true);
+
+            var data = indexify(index.i, data);
+
+            // initialize the starting yDomain for the not-rescale case after indexify (to have calculated point.display)
+            if (typeof(currentYDomain) === "undefined") {
+                currentYDomain = getCurrentYDomain(data);
+            }
+
+            if (!rescaleY) {
+                lines.yDomain(currentYDomain);
+                lines.clipEdge(true);
+            } else {
+                lines.yDomain(null);
+            }
+
+            // Setup containers and skeleton of chart
+            var interactivePointerEvents = (useInteractiveGuideline) ? "none" : "all";
+            var wrap = container.selectAll('g.nv-wrap.nv-cumulativeLine').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-cumulativeLine').append('g');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-interactive');
+            gEnter.append('g').attr('class', 'nv-x nv-axis').style("pointer-events","none");
+            gEnter.append('g').attr('class', 'nv-y nv-axis');
+            gEnter.append('g').attr('class', 'nv-background');
+            gEnter.append('g').attr('class', 'nv-linesWrap').style("pointer-events",interactivePointerEvents);
+            gEnter.append('g').attr('class', 'nv-avgLinesWrap').style("pointer-events","none");
+            gEnter.append('g').attr('class', 'nv-legendWrap');
+            gEnter.append('g').attr('class', 'nv-controlsWrap');
+
+            // Legend
+            if (!showLegend) {
+                g.select('.nv-legendWrap').selectAll('*').remove();
+            } else {
+                legend.width(availableWidth);
+
+                g.select('.nv-legendWrap')
+                    .datum(data)
+                    .call(legend);
+
+                if (!marginTop && legend.height() !== margin.top) {
+                    margin.top = legend.height();
+                    availableHeight = nv.utils.availableHeight(height, container, margin);
+                }
+
+                g.select('.nv-legendWrap')
+                    .attr('transform', 'translate(0,' + (-margin.top) +')')
+            }
+
+            // Controls
+            if (!showControls) {
+                 g.select('.nv-controlsWrap').selectAll('*').remove();
+            } else {
+                var controlsData = [
+                    { key: 'Re-scale y-axis', disabled: !rescaleY }
+                ];
+
+                controls
+                    .width(140)
+                    .color(['#444', '#444', '#444'])
+                    .rightAlign(false)
+                    .margin({top: 5, right: 0, bottom: 5, left: 20})
+                ;
+
+                g.select('.nv-controlsWrap')
+                    .datum(controlsData)
+                    .attr('transform', 'translate(0,' + (-margin.top) +')')
+                    .call(controls);
+            }
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            if (rightAlignYAxis) {
+                g.select(".nv-y.nv-axis")
+                    .attr("transform", "translate(" + availableWidth + ",0)");
+            }
+
+            // Show error if index point value is 0 (division by zero avoided)
+            var tempDisabled = data.filter(function(d) { return d.tempDisabled });
+
+            wrap.select('.tempDisabled').remove(); //clean-up and prevent duplicates
+            if (tempDisabled.length) {
+                wrap.append('text').attr('class', 'tempDisabled')
+                    .attr('x', availableWidth / 2)
+                    .attr('y', '-.71em')
+                    .style('text-anchor', 'end')
+                    .text(tempDisabled.map(function(d) { return d.key }).join(', ') + ' values cannot be calculated for this time period.');
+            }
+
+            //Set up interactive layer
+            if (useInteractiveGuideline) {
+                interactiveLayer
+                    .width(availableWidth)
+                    .height(availableHeight)
+                    .margin({left:margin.left,top:margin.top})
+                    .svgContainer(container)
+                    .xScale(x);
+                wrap.select(".nv-interactive").call(interactiveLayer);
+            }
+
+            gEnter.select('.nv-background')
+                .append('rect');
+
+            g.select('.nv-background rect')
+                .attr('width', availableWidth)
+                .attr('height', availableHeight);
+
+            lines
+                //.x(function(d) { return d.x })
+                .y(function(d) { return d.display.y })
+                .width(availableWidth)
+                .height(availableHeight)
+                .color(data.map(function(d,i) {
+                    return d.color || color(d, i);
+                }).filter(function(d,i) { return !data[i].disabled && !data[i].tempDisabled; }));
+
+            var linesWrap = g.select('.nv-linesWrap')
+                .datum(data.filter(function(d) { return  !d.disabled && !d.tempDisabled }));
+
+            linesWrap.call(lines);
+
+            //Store a series index number in the data array.
+            data.forEach(function(d,i) {
+                d.seriesIndex = i;
+            });
+
+            var avgLineData = data.filter(function(d) {
+                return !d.disabled && !!average(d);
+            });
+
+            var avgLines = g.select(".nv-avgLinesWrap").selectAll("line")
+                .data(avgLineData, function(d) { return d.key; });
+
+            var getAvgLineY = function(d) {
+                //If average lines go off the svg element, clamp them to the svg bounds.
+                var yVal = y(average(d));
+                if (yVal < 0) return 0;
+                if (yVal > availableHeight) return availableHeight;
+                return yVal;
+            };
+
+            avgLines.enter()
+                .append('line')
+                .style('stroke-width',2)
+                .style('stroke-dasharray','10,10')
+                .style('stroke',function (d,i) {
+                    return lines.color()(d,d.seriesIndex);
+                })
+                .attr('x1',0)
+                .attr('x2',availableWidth)
+                .attr('y1', getAvgLineY)
+                .attr('y2', getAvgLineY);
+
+            avgLines
+                .style('stroke-opacity',function(d){
+                    //If average lines go offscreen, make them transparent
+                    var yVal = y(average(d));
+                    if (yVal < 0 || yVal > availableHeight) return 0;
+                    return 1;
+                })
+                .attr('x1',0)
+                .attr('x2',availableWidth)
+                .attr('y1', getAvgLineY)
+                .attr('y2', getAvgLineY);
+
+            avgLines.exit().remove();
+
+            //Create index line
+            var indexLine = linesWrap.selectAll('.nv-indexLine')
+                .data([index]);
+            indexLine.enter().append('rect').attr('class', 'nv-indexLine')
+                .attr('width', 3)
+                .attr('x', -2)
+                .attr('fill', 'red')
+                .attr('fill-opacity', .5)
+                .style("pointer-events","all")
+                .call(indexDrag);
+
+            indexLine
+                .attr('transform', function(d) { return 'translate(' + dx(d.i) + ',0)' })
+                .attr('height', availableHeight);
+
+            // Setup Axes
+            if (showXAxis) {
+                xAxis
+                    .scale(x)
+                    ._ticks( nv.utils.calcTicksX(availableWidth/70, data) )
+                    .tickSize(-availableHeight, 0);
+
+                g.select('.nv-x.nv-axis')
+                    .attr('transform', 'translate(0,' + y.range()[0] + ')');
+                g.select('.nv-x.nv-axis')
+                    .call(xAxis);
+            }
+
+            if (showYAxis) {
+                yAxis
+                    .scale(y)
+                    ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+                    .tickSize( -availableWidth, 0);
+
+                g.select('.nv-y.nv-axis')
+                    .call(yAxis);
+            }
+
+            //============================================================
+            // Event Handling/Dispatching (in chart's scope)
+            //------------------------------------------------------------
+
+            function updateZero() {
+                indexLine
+                    .data([index]);
+
+                //When dragging the index line, turn off line transitions.
+                // Then turn them back on when done dragging.
+                var oldDuration = chart.duration();
+                chart.duration(0);
+                chart.update();
+                chart.duration(oldDuration);
+            }
+
+            g.select('.nv-background rect')
+                .on('click', function() {
+                    index.x = d3.mouse(this)[0];
+                    index.i = Math.round(dx.invert(index.x));
+
+                    // update state and send stateChange with new index
+                    state.index = index.i;
+                    dispatch.stateChange(state);
+
+                    updateZero();
+                });
+
+            lines.dispatch.on('elementClick', function(e) {
+                index.i = e.pointIndex;
+                index.x = dx(index.i);
+
+                // update state and send stateChange with new index
+                state.index = index.i;
+                dispatch.stateChange(state);
+
+                updateZero();
+            });
+
+            controls.dispatch.on('legendClick', function(d,i) {
+                d.disabled = !d.disabled;
+                rescaleY = !d.disabled;
+                state.rescaleY = rescaleY;
+                if (!rescaleY) {
+                    currentYDomain = getCurrentYDomain(data); // rescale is turned off, so set the currentYDomain
+                }
+                dispatch.stateChange(state);
+                chart.update();
+            });
+
+            legend.dispatch.on('stateChange', function(newState) {
+                for (var key in newState)
+                    state[key] = newState[key];
+                dispatch.stateChange(state);
+                chart.update();
+            });
+
+            interactiveLayer.dispatch.on('elementMousemove', function(e) {
+                lines.clearHighlights();
+                var singlePoint, pointIndex, pointXLocation, allData = [];
+
+                data
+                    .filter(function(series, i) {
+                        series.seriesIndex = i;
+                        return !(series.disabled || series.tempDisabled);
+                    })
+                    .forEach(function(series,i) {
+                        pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+                        lines.highlightPoint(i, pointIndex, true);
+                        var point = series.values[pointIndex];
+                        if (typeof point === 'undefined') return;
+                        if (typeof singlePoint === 'undefined') singlePoint = point;
+                        if (typeof pointXLocation === 'undefined') pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+                        allData.push({
+                            key: series.key,
+                            value: chart.y()(point, pointIndex),
+                            color: color(series,series.seriesIndex)
+                        });
+                    });
+
+                //Highlight the tooltip entry based on which point the mouse is closest to.
+                if (allData.length > 2) {
+                    var yValue = chart.yScale().invert(e.mouseY);
+                    var domainExtent = Math.abs(chart.yScale().domain()[0] - chart.yScale().domain()[1]);
+                    var threshold = 0.03 * domainExtent;
+                    var indexToHighlight = nv.nearestValueIndex(allData.map(function(d){return d.value}),yValue,threshold);
+                    if (indexToHighlight !== null)
+                        allData[indexToHighlight].highlight = true;
+                }
+
+                var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex), pointIndex);
+                interactiveLayer.tooltip
+                    .valueFormatter(function(d,i) {
+                        return yAxis.tickFormat()(d);
+                    })
+                    .data(
+                    {
+                        value: xValue,
+                        series: allData
+                    }
+                )();
+
+                interactiveLayer.renderGuideLine(pointXLocation);
+            });
+
+            interactiveLayer.dispatch.on("elementMouseout",function(e) {
+                lines.clearHighlights();
+            });
+
+            // Update chart from a state object passed to event handler
+            dispatch.on('changeState', function(e) {
+                if (typeof e.disabled !== 'undefined') {
+                    data.forEach(function(series,i) {
+                        series.disabled = e.disabled[i];
+                    });
+
+                    state.disabled = e.disabled;
+                }
+
+                if (typeof e.index !== 'undefined') {
+                    index.i = e.index;
+                    index.x = dx(index.i);
+
+                    state.index = e.index;
+
+                    indexLine
+                        .data([index]);
+                }
+
+                if (typeof e.rescaleY !== 'undefined') {
+                    rescaleY = e.rescaleY;
+                }
+
+                chart.update();
+            });
+
+        });
+
+        renderWatch.renderEnd('cumulativeLineChart immediate');
+
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    lines.dispatch.on('elementMouseover.tooltip', function(evt) {
+        var point = {
+            x: chart.x()(evt.point),
+            y: chart.y()(evt.point),
+            color: evt.point.color
+        };
+        evt.point = point;
+        tooltip.data(evt).hidden(false);
+    });
+
+    lines.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.hidden(true)
+    });
+
+    //============================================================
+    // Functions
+    //------------------------------------------------------------
+
+    var indexifyYGetter = null;
+    /* Normalize the data according to an index point. */
+    function indexify(idx, data) {
+        if (!indexifyYGetter) indexifyYGetter = lines.y();
+        return data.map(function(line, i) {
+            if (!line.values) {
+                return line;
+            }
+            var indexValue = line.values[idx];
+            if (indexValue == null) {
+                return line;
+            }
+            var v = indexifyYGetter(indexValue, idx);
+
+            // avoid divide by zero
+            if (Math.abs(v) < 0.00001 && !noErrorCheck) {
+                line.tempDisabled = true;
+                return line;
+            }
+
+            line.tempDisabled = false;
+
+            line.values = line.values.map(function(point, pointIndex) {
+                point.display = {'y': (indexifyYGetter(point, pointIndex) - v) / v };
+                return point;
+            });
+
+            return line;
+        })
+    }
+
+    function getCurrentYDomain(data) {
+        var seriesDomains = data
+            .filter(function(series) { return !(series.disabled || series.tempDisabled)})
+            .map(function(series,i) {
+                return d3.extent(series.values, function (d) { return d.display.y });
+            });
+
+        return [
+            d3.min(seriesDomains, function(d) { return d[0] }),
+            d3.max(seriesDomains, function(d) { return d[1] })
+        ];
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    // expose chart's sub-components
+    chart.dispatch = dispatch;
+    chart.lines = lines;
+    chart.legend = legend;
+    chart.controls = controls;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.interactiveLayer = interactiveLayer;
+    chart.state = state;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+        showControls:     {get: function(){return showControls;}, set: function(_){showControls=_;}},
+        showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+        average: {get: function(){return average;}, set: function(_){average=_;}},
+        defaultState:    {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+        showXAxis:    {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis:    {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        noErrorCheck:    {get: function(){return noErrorCheck;}, set: function(_){noErrorCheck=_;}},
+
+        // options that require extra logic in the setter
+        rescaleY:     {get: function(){return rescaleY;}, set: function(_){
+            rescaleY = _;
+            chart.state.rescaleY = _; // also update state
+        }},
+        margin: {get: function(){return margin;}, set: function(_){
+            if (_.top !== undefined) {
+                margin.top = _.top;
+                marginTop = _.top;
+            }
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+            legend.color(color);
+        }},
+        useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+            useInteractiveGuideline = _;
+            if (_ === true) {
+                chart.interactive(false);
+                chart.useVoronoi(false);
+            }
+        }},
+        rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+            rightAlignYAxis = _;
+            yAxis.orient( (_) ? 'right' : 'left');
+        }},
+        duration:    {get: function(){return duration;}, set: function(_){
+            duration = _;
+            lines.duration(duration);
+            xAxis.duration(duration);
+            yAxis.duration(duration);
+            renderWatch.reset(duration);
+        }}
+    });
+
+    nv.utils.inheritOptions(chart, lines);
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+//TODO: consider deprecating by adding necessary features to multiBar model
+nv.models.discreteBar = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , width = 960
+        , height = 500
+        , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+        , container
+        , x = d3.scale.ordinal()
+        , y = d3.scale.linear()
+        , getX = function(d) { return d.x }
+        , getY = function(d) { return d.y }
+        , forceY = [0] // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove
+        , color = nv.utils.defaultColor()
+        , showValues = false
+        , valueFormat = d3.format(',.2f')
+        , xDomain
+        , yDomain
+        , xRange
+        , yRange
+        , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+        , rectClass = 'discreteBar'
+        , duration = 250
+        ;
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var x0, y0;
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        selection.each(function(data) {
+            var availableWidth = width - margin.left - margin.right,
+                availableHeight = height - margin.top - margin.bottom;
+
+            container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            //add series index to each data point for reference
+            data.forEach(function(series, i) {
+                series.values.forEach(function(point) {
+                    point.series = i;
+                });
+            });
+
+            // Setup Scales
+            // remap and flatten the data for use in calculating the scales' domains
+            var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate
+                data.map(function(d) {
+                    return d.values.map(function(d,i) {
+                        return { x: getX(d,i), y: getY(d,i), y0: d.y0 }
+                    })
+                });
+
+            x   .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x }))
+                .rangeBands(xRange || [0, availableWidth], .1);
+            y   .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y }).concat(forceY)));
+
+            // If showValues, pad the Y axis range to account for label height
+            if (showValues) y.range(yRange || [availableHeight - (y.domain()[0] < 0 ? 12 : 0), y.domain()[1] > 0 ? 12 : 0]);
+            else y.range(yRange || [availableHeight, 0]);
+
+            //store old scales if they exist
+            x0 = x0 || x;
+            y0 = y0 || y.copy().range([y(0),y(0)]);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-discretebar').data([data]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discretebar');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-groups');
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            //TODO: by definition, the discrete bar should not have multiple groups, will modify/remove later
+            var groups = wrap.select('.nv-groups').selectAll('.nv-group')
+                .data(function(d) { return d }, function(d) { return d.key });
+            groups.enter().append('g')
+                .style('stroke-opacity', 1e-6)
+                .style('fill-opacity', 1e-6);
+            groups.exit()
+                .watchTransition(renderWatch, 'discreteBar: exit groups')
+                .style('stroke-opacity', 1e-6)
+                .style('fill-opacity', 1e-6)
+                .remove();
+            groups
+                .attr('class', function(d,i) { return 'nv-group nv-series-' + i })
+                .classed('hover', function(d) { return d.hover });
+            groups
+                .watchTransition(renderWatch, 'discreteBar: groups')
+                .style('stroke-opacity', 1)
+                .style('fill-opacity', .75);
+
+            var bars = groups.selectAll('g.nv-bar')
+                .data(function(d) { return d.values });
+            bars.exit().remove();
+
+            var barsEnter = bars.enter().append('g')
+                .attr('transform', function(d,i,j) {
+                    return 'translate(' + (x(getX(d,i)) + x.rangeBand() * .05 ) + ', ' + y(0) + ')'
+                })
+                .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here
+                    d3.select(this).classed('hover', true);
+                    dispatch.elementMouseover({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                })
+                .on('mouseout', function(d,i) {
+                    d3.select(this).classed('hover', false);
+                    dispatch.elementMouseout({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                })
+                .on('mousemove', function(d,i) {
+                    dispatch.elementMousemove({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                })
+                .on('click', function(d,i) {
+                    var element = this;
+                    dispatch.elementClick({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill"),
+                        event: d3.event,
+                        element: element
+                    });
+                    d3.event.stopPropagation();
+                })
+                .on('dblclick', function(d,i) {
+                    dispatch.elementDblClick({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                    d3.event.stopPropagation();
+                });
+
+            barsEnter.append('rect')
+                .attr('height', 0)
+                .attr('width', x.rangeBand() * .9 / data.length )
+
+            if (showValues) {
+                barsEnter.append('text')
+                    .attr('text-anchor', 'middle')
+                ;
+
+                bars.select('text')
+                    .text(function(d,i) { return valueFormat(getY(d,i)) })
+                    .watchTransition(renderWatch, 'discreteBar: bars text')
+                    .attr('x', x.rangeBand() * .9 / 2)
+                    .attr('y', function(d,i) { return getY(d,i) < 0 ? y(getY(d,i)) - y(0) + 12 : -4 })
+
+                ;
+            } else {
+                bars.selectAll('text').remove();
+            }
+
+            bars
+                .attr('class', function(d,i) { return getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive' })
+                .style('fill', function(d,i) { return d.color || color(d,i) })
+                .style('stroke', function(d,i) { return d.color || color(d,i) })
+                .select('rect')
+                .attr('class', rectClass)
+                .watchTransition(renderWatch, 'discreteBar: bars rect')
+                .attr('width', x.rangeBand() * .9 / data.length);
+            bars.watchTransition(renderWatch, 'discreteBar: bars')
+                //.delay(function(d,i) { return i * 1200 / data[0].values.length })
+                .attr('transform', function(d,i) {
+                    var left = x(getX(d,i)) + x.rangeBand() * .05,
+                        top = getY(d,i) < 0 ?
+                            y(0) :
+                                y(0) - y(getY(d,i)) < 1 ?
+                            y(0) - 1 : //make 1 px positive bars show up above y=0
+                            y(getY(d,i));
+
+                    return 'translate(' + left + ', ' + top + ')'
+                })
+                .select('rect')
+                .attr('height', function(d,i) {
+                    return  Math.max(Math.abs(y(getY(d,i)) - y(0)), 1)
+                });
+
+
+            //store old scales for use in transitions on update
+            x0 = x.copy();
+            y0 = y.copy();
+
+        });
+
+        renderWatch.renderEnd('discreteBar immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:   {get: function(){return width;}, set: function(_){width=_;}},
+        height:  {get: function(){return height;}, set: function(_){height=_;}},
+        forceY:  {get: function(){return forceY;}, set: function(_){forceY=_;}},
+        showValues: {get: function(){return showValues;}, set: function(_){showValues=_;}},
+        x:       {get: function(){return getX;}, set: function(_){getX=_;}},
+        y:       {get: function(){return getY;}, set: function(_){getY=_;}},
+        xScale:  {get: function(){return x;}, set: function(_){x=_;}},
+        yScale:  {get: function(){return y;}, set: function(_){y=_;}},
+        xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+        yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+        xRange:  {get: function(){return xRange;}, set: function(_){xRange=_;}},
+        yRange:  {get: function(){return yRange;}, set: function(_){yRange=_;}},
+        valueFormat:    {get: function(){return valueFormat;}, set: function(_){valueFormat=_;}},
+        id:          {get: function(){return id;}, set: function(_){id=_;}},
+        rectClass: {get: function(){return rectClass;}, set: function(_){rectClass=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+
+nv.models.discreteBarChart = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var discretebar = nv.models.discreteBar()
+        , xAxis = nv.models.axis()
+        , yAxis = nv.models.axis()
+    , legend = nv.models.legend()
+        , tooltip = nv.models.tooltip()
+        ;
+
+    var margin = {top: 15, right: 10, bottom: 50, left: 60}
+        , marginTop = null
+        , width = null
+        , height = null
+        , color = nv.utils.getColor()
+    , showLegend = false
+        , showXAxis = true
+        , showYAxis = true
+        , rightAlignYAxis = false
+        , staggerLabels = false
+        , wrapLabels = false
+        , rotateLabels = 0
+        , x
+        , y
+        , noData = null
+        , dispatch = d3.dispatch('beforeUpdate','renderEnd')
+        , duration = 250
+        ;
+
+    xAxis
+        .orient('bottom')
+        .showMaxMin(false)
+        .tickFormat(function(d) { return d })
+    ;
+    yAxis
+        .orient((rightAlignYAxis) ? 'right' : 'left')
+        .tickFormat(d3.format(',.1f'))
+    ;
+
+    tooltip
+        .duration(0)
+        .headerEnabled(false)
+        .valueFormatter(function(d, i) {
+            return yAxis.tickFormat()(d, i);
+        })
+        .keyFormatter(function(d, i) {
+            return xAxis.tickFormat()(d, i);
+        });
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        renderWatch.models(discretebar);
+        if (showXAxis) renderWatch.models(xAxis);
+        if (showYAxis) renderWatch.models(yAxis);
+
+        selection.each(function(data) {
+            var container = d3.select(this),
+                that = this;
+            nv.utils.initSVG(container);
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = nv.utils.availableHeight(height, container, margin);
+
+            chart.update = function() {
+                dispatch.beforeUpdate();
+                container.transition().duration(duration).call(chart);
+            };
+            chart.container = this;
+
+            // Display No Data message if there's nothing to show.
+            if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+                nv.utils.noData(chart, container);
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            // Setup Scales
+            x = discretebar.xScale();
+            y = discretebar.yScale().clamp(true);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-discreteBarWithAxes').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discreteBarWithAxes').append('g');
+            var defsEnter = gEnter.append('defs');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-x nv-axis');
+            gEnter.append('g').attr('class', 'nv-y nv-axis')
+                .append('g').attr('class', 'nv-zeroLine')
+                .append('line');
+
+            gEnter.append('g').attr('class', 'nv-barsWrap');
+        gEnter.append('g').attr('class', 'nv-legendWrap');
+
+            g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            // Legend
+            if (!showLegend) {
+                g.select('.nv-legendWrap').selectAll('*').remove();
+            } else {
+                legend.width(availableWidth);
+
+                g.select('.nv-legendWrap')
+                    .datum(data)
+                    .call(legend);
+
+                if (!marginTop && legend.height() !== margin.top) {
+                    margin.top = legend.height();
+                    availableHeight = nv.utils.availableHeight(height, container, margin);
+                }
+
+                wrap.select('.nv-legendWrap')
+                    .attr('transform', 'translate(0,' + (-margin.top) +')')
+            }
+
+            if (rightAlignYAxis) {
+                g.select(".nv-y.nv-axis")
+                    .attr("transform", "translate(" + availableWidth + ",0)");
+            }
+
+            // Main Chart Component(s)
+            discretebar
+                .width(availableWidth)
+                .height(availableHeight);
+
+            var barsWrap = g.select('.nv-barsWrap')
+                .datum(data.filter(function(d) { return !d.disabled }));
+
+            barsWrap.transition().call(discretebar);
+
+
+            defsEnter.append('clipPath')
+                .attr('id', 'nv-x-label-clip-' + discretebar.id())
+                .append('rect');
+
+            g.select('#nv-x-label-clip-' + discretebar.id() + ' rect')
+                .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1))
+                .attr('height', 16)
+                .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 ));
+
+            // Setup Axes
+            if (showXAxis) {
+                xAxis
+                    .scale(x)
+                    ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+                    .tickSize(-availableHeight, 0);
+
+                g.select('.nv-x.nv-axis')
+                    .attr('transform', 'translate(0,' + (y.range()[0] + ((discretebar.showValues() && y.domain()[0] < 0) ? 16 : 0)) + ')');
+                g.select('.nv-x.nv-axis').call(xAxis);
+
+                var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
+                if (staggerLabels) {
+                    xTicks
+                        .selectAll('text')
+                        .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 == 0 ? '5' : '17') + ')' })
+                }
+
+                if (rotateLabels) {
+                    xTicks
+                        .selectAll('.tick text')
+                        .attr('transform', 'rotate(' + rotateLabels + ' 0,0)')
+                        .style('text-anchor', rotateLabels > 0 ? 'start' : 'end');
+                }
+
+                if (wrapLabels) {
+                    g.selectAll('.tick text')
+                        .call(nv.utils.wrapTicks, chart.xAxis.rangeBand())
+                }
+            }
+
+            if (showYAxis) {
+                yAxis
+                    .scale(y)
+                    ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+                    .tickSize( -availableWidth, 0);
+
+                g.select('.nv-y.nv-axis').call(yAxis);
+            }
+
+            // Zero line
+            g.select(".nv-zeroLine line")
+                .attr("x1",0)
+                .attr("x2",(rightAlignYAxis) ? -availableWidth : availableWidth)
+                .attr("y1", y(0))
+                .attr("y2", y(0))
+            ;
+        });
+
+        renderWatch.renderEnd('discreteBar chart immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    discretebar.dispatch.on('elementMouseover.tooltip', function(evt) {
+        evt['series'] = {
+            key: chart.x()(evt.data),
+            value: chart.y()(evt.data),
+            color: evt.color
+        };
+        tooltip.data(evt).hidden(false);
+    });
+
+    discretebar.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.hidden(true);
+    });
+
+    discretebar.dispatch.on('elementMousemove.tooltip', function(evt) {
+        tooltip();
+    });
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.discretebar = discretebar;
+    chart.legend = legend;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+    showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+        staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+        rotateLabels:  {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
+        wrapLabels:  {get: function(){return wrapLabels;}, set: function(_){wrapLabels=!!_;}},
+        showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            if (_.top !== undefined) {
+                margin.top = _.top;
+                marginTop = _.top;
+            }
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+            discretebar.duration(duration);
+            xAxis.duration(duration);
+            yAxis.duration(duration);
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+            discretebar.color(color);
+        legend.color(color);
+        }},
+        rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+            rightAlignYAxis = _;
+            yAxis.orient( (_) ? 'right' : 'left');
+        }}
+    });
+
+    nv.utils.inheritOptions(chart, discretebar);
+    nv.utils.initOptions(chart);
+
+    return chart;
+}
+
+nv.models.distribution = function() {
+    "use strict";
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , width = 400 //technically width or height depending on x or y....
+        , size = 8
+        , axis = 'x' // 'x' or 'y'... horizontal or vertical
+        , getData = function(d) { return d[axis] }  // defaults d.x or d.y
+        , color = nv.utils.defaultColor()
+        , scale = d3.scale.linear()
+        , domain
+        , duration = 250
+        , dispatch = d3.dispatch('renderEnd')
+        ;
+
+    //============================================================
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var scale0;
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    //============================================================
+
+
+    function chart(selection) {
+        renderWatch.reset();
+        selection.each(function(data) {
+            var availableLength = width - (axis === 'x' ? margin.left + margin.right : margin.top + margin.bottom),
+                naxis = axis == 'x' ? 'y' : 'x',
+                container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            //------------------------------------------------------------
+            // Setup Scales
+
+            scale0 = scale0 || scale;
+
+            //------------------------------------------------------------
+
+
+            //------------------------------------------------------------
+            // Setup containers and skeleton of chart
+
+            var wrap = container.selectAll('g.nv-distribution').data([data]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-distribution');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+            //------------------------------------------------------------
+
+
+            var distWrap = g.selectAll('g.nv-dist')
+                .data(function(d) { return d }, function(d) { return d.key });
+
+            distWrap.enter().append('g');
+            distWrap
+                .attr('class', function(d,i) { return 'nv-dist nv-series-' + i })
+                .style('stroke', function(d,i) { return color(d, i) });
+
+            var dist = distWrap.selectAll('line.nv-dist' + axis)
+                .data(function(d) { return d.values })
+            dist.enter().append('line')
+                .attr(axis + '1', function(d,i) { return scale0(getData(d,i)) })
+                .attr(axis + '2', function(d,i) { return scale0(getData(d,i)) })
+            renderWatch.transition(distWrap.exit().selectAll('line.nv-dist' + axis), 'dist exit')
+                // .transition()
+                .attr(axis + '1', function(d,i) { return scale(getData(d,i)) })
+                .attr(axis + '2', function(d,i) { return scale(getData(d,i)) })
+                .style('stroke-opacity', 0)
+                .remove();
+            dist
+                .attr('class', function(d,i) { return 'nv-dist' + axis + ' nv-dist' + axis + '-' + i })
+                .attr(naxis + '1', 0)
+                .attr(naxis + '2', size);
+            renderWatch.transition(dist, 'dist')
+                // .transition()
+                .attr(axis + '1', function(d,i) { return scale(getData(d,i)) })
+                .attr(axis + '2', function(d,i) { return scale(getData(d,i)) })
+
+
+            scale0 = scale.copy();
+
+        });
+        renderWatch.renderEnd('distribution immediate');
+        return chart;
+    }
+
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+    chart.options = nv.utils.optionsFunc.bind(chart);
+    chart.dispatch = dispatch;
+
+    chart.margin = function(_) {
+        if (!arguments.length) return margin;
+        margin.top    = typeof _.top    != 'undefined' ? _.top    : margin.top;
+        margin.right  = typeof _.right  != 'undefined' ? _.right  : margin.right;
+        margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom;
+        margin.left   = typeof _.left   != 'undefined' ? _.left   : margin.left;
+        return chart;
+    };
+
+    chart.width = function(_) {
+        if (!arguments.length) return width;
+        width = _;
+        return chart;
+    };
+
+    chart.axis = function(_) {
+        if (!arguments.length) return axis;
+        axis = _;
+        return chart;
+    };
+
+    chart.size = function(_) {
+        if (!arguments.length) return size;
+        size = _;
+        return chart;
+    };
+
+    chart.getData = function(_) {
+        if (!arguments.length) return getData;
+        getData = d3.functor(_);
+        return chart;
+    };
+
+    chart.scale = function(_) {
+        if (!arguments.length) return scale;
+        scale = _;
+        return chart;
+    };
+
+    chart.color = function(_) {
+        if (!arguments.length) return color;
+        color = nv.utils.getColor(_);
+        return chart;
+    };
+
+    chart.duration = function(_) {
+        if (!arguments.length) return duration;
+        duration = _;
+        renderWatch.reset(duration);
+        return chart;
+    };
+    //============================================================
+
+
+    return chart;
+}
+nv.models.distroPlot = function() {
+    "use strict";
+
+    // IMPROVEMENTS:
+    // - cleanup tooltip to look like candlestick example (don't need color square for everything)
+    // - extend y scale range to min/max data better visually
+    // - tips of violins need to be cut off if very long
+    // - transition from box to violin not great since box only has a few points, and violin has many - need to generate box with as many points as violin
+    // - when providing colorGroup, should color boxes by either parent or child group category (e.g. isolator)
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0},
+        width = 960,
+        height = 500,
+        id = Math.floor(Math.random() * 10000), // Create semi-unique ID in case user doesn't select one
+        xScale = d3.scale.ordinal(),
+        yScale = d3.scale.linear(),
+        getX  = function(d) { return d.label }, // Default data model selectors.
+        getY  = function(d) { return d.value },
+        getColor = function(d) { return d.color },
+        getQ1 = function(d) { return d.values.q1 },
+        getQ2 = function(d) { return d.values.q2 },
+        getQ3 = function(d) { return d.values.q3 },
+        getNl = function(d) { return (centralTendency == 'mean' ? getMean(d) : getQ2(d)) - d.values.notch },
+        getNu = function(d) { return (centralTendency == 'mean' ? getMean(d) : getQ2(d)) + d.values.notch },
+        getMean = function(d) { return d.values.mean },
+        getWl = function(d) { return d.values.wl[whiskerDef] },
+        getWh = function(d) { return d.values.wu[whiskerDef] },
+        getMin = function(d) { return d.values.min },
+        getMax = function(d) { return d.values.max },
+        getDev = function(d) { return d.values.dev },
+        getValsObj = function(d) { return d.values.observations; },
+        getValsArr = function(d) { return d.values.observations.map(function(e) { return e.y }); },
+        plotType, // type of background: 'box', 'violin', 'none'/false - default: 'box' - 'none' will activate random scatter automatically
+        observationType = false, // type of observations to show: 'random', 'swarm', 'line', 'centered' - default: false (don't show any observations, even if an outlier)
+        whiskerDef = 'iqr', // type of whisker to render: 'iqr', 'minmax', 'stddev' - default: iqr
+        hideWhiskers = false,
+        notchBox = false, // bool whether to notch box
+        colorGroup = false, // if specified, each x-category will be split into groups, each colored
+        centralTendency = false,
+        showOnlyOutliers = true, // show only outliers in box plot
+        jitter = 0.7, // faction of that jitter should take up in 'random' observationType, must be in range [0,1]; see jitterX(), default 0.7
+        squash = true, // whether to remove the x-axis positions for empty data groups, default is true
+        bandwidth = 'scott', // bandwidth for kde calculation, can be float or str, if str, must be one of scott or silverman
+        clampViolin = true, // whether to clamp the "tails" of the violin; prevents long 0-density area
+        resolution = 50,
+        pointSize = 3,
+        color = nv.utils.defaultColor(),
+        container = null,
+        xDomain, xRange,
+        yDomain, yRange,
+        dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd'),
+        duration = 250,
+        maxBoxWidth = null;
+
+    //============================================================
+    // Helper Functions
+    //------------------------------------------------------------
+
+
+    /* Returns the smaller of std(X, ddof=1) or normalized IQR(X) over axis 0.
+     *
+     * @param (list) x - input x formatted as a single list of values
+     *
+     * @return float
+     *
+     * Source: https://github.com/statsmodels/statsmodels/blob/master/statsmodels/nonparametric/bandwidths.py#L9
+     */
+    function select_sigma(x) {
+        var sorted = x.sort(d3.ascending); // sort our dat
+        var normalize = 1.349;
+        var IQR = (d3.quantile(sorted, 0.75) - d3.quantile(sorted, 0.25))/normalize; // normalized IQR
+        return d3.min([d3.deviation(sorted), IQR]);
+    }
+
+    /*
+    Scott's Rule of Thumb
+
+    Parameters
+    ----------
+    x : array-like
+        Array for which to get the bandwidth
+    type : string
+           The type of estimate to use, must be one of scott or silverman
+
+    Returns
+    -------
+    bw : float
+        The estimate of the bandwidth
+
+    Notes
+    -----
+    Returns 1.059 * A * n ** (-1/5.) where ::
+       A = min(std(x, ddof=1), IQR/1.349)
+       IQR = np.subtract.reduce(np.percentile(x, [75,25]))
+
+    References
+    ----------
+    Scott, D.W. (1992) Multivariate Density Estimation: Theory, Practice, and
+        Visualization.
+     */
+    function calcBandwidth(x, type) {
+
+        if (typeof type === 'undefined') type = 'scott';
+
+        // TODO: consider using https://github.com/jasondavies/science.js
+        var A = select_sigma(x);
+        var n = x.length;
+        return type==='scott' ? Math.pow(1.059 * A * n, -0.2) : Math.pow(.9 * A * n, -0.2);
+    }
+
+
+
+    /*
+     * Prep data for use with distroPlot by grouping data
+     * by .x() option set by user and then calculating
+     * count, sum, mean, q1, q2 (median), q3, lower whisker (wl)
+     * upper whisker (wu), iqr, min, max, and standard dev.
+     *
+     * NOTE: preparing this data can be resource intensive, and
+     *       is therefore only run once on plot load. It can
+     *       manually be run by calling recalcData(). This should
+     *       be re-run any time the axis accessors are changed or
+     *       when bandwidth/resolution are updated.
+     *
+     * NOTE: this will also setup the individual vertical scales
+     *       for the violins.
+     *
+     * @param (list) dat - input data formatted as list of objects,
+     *   with an object key that must exist when accessed by getX()
+     *
+     * @return prepared data in the form for box plotType:
+     * [{
+     *    key : YY,
+     *    values: {
+     *      count: XX,
+     *      sum: XX,
+     *      mean: XX,
+     *      q1: XX,
+     *      q2: XX,
+     *      q3: XX,
+     *      wl: XX,
+     *      wu: XX,
+     *      iqr: XX,
+     *      min: XX,
+     *      max: XX,
+     *      dev: XX,
+     *      observations: [{y:XX,..},..],
+     *      key: XX,
+     *      kdeDat: XX,
+     *      notch: XX,
+     *    }
+     *  },
+     *  ...
+     *  ]
+     * for violin plotType:
+     * [{
+     *    key : YY,
+     *    values: {
+     *      original: [{y:XX,..},..]
+     *    }
+     *  },
+     *  ...
+     *  ]
+     * where YY are those keys in dat that define the
+     * x-axis and which are defined by .x()
+     */
+    function prepData(dat) {
+
+        // helper function to calcuate the various boxplot stats
+        function calcStats(g, xGroup) {
+
+            // sort data by Y so we can calc quartiles
+            var v = g.map(function(d) {
+                if (colorGroup) allColorGroups.add(colorGroup(d)); // list of all colorGroups; used to set x-axis
+                return getY(d);
+            }).sort(d3.ascending);
+
+            var q1 = d3.quantile(v, 0.25);
+            var q3 = d3.quantile(v, 0.75);
+            var iqr = q3 - q1;
+            var upper = q3 + 1.5 * iqr;
+            var lower = q1 - 1.5 * iqr;
+
+            /* whisker definitions:
+             *  - iqr: also known as Tukey boxplot, the lowest datum still within 1.5 IQR of the lower quartile, and the highest datum still within 1.5 IQR of the upper quartile
+             *  - minmax: the minimum and maximum of all of the data
+             *  - sttdev: one standard deviation above and below the mean of the data
+             * Note that the central tendency type (median or mean) does not impact the whisker location
+             */
+            var wl = {iqr: d3.max([d3.min(v),  d3.min(v.filter(function(d) {return d > lower}))]), minmax: d3.min(v), stddev: d3.mean(v) - d3.deviation(v)};
+            var wu = {iqr: d3.min([d3.max(v), d3.max(v.filter(function(d) {return d < upper}))]), minmax: d3.max(v), stddev: d3.mean(v) + d3.deviation(v)};
+            var median = d3.median(v);
+            var mean = d3.mean(v);
+            var observations = [];
+
+
+            // d3-beeswarm library must be externally loaded if being used
+            // https://github.com/Kcnarf/d3-beeswarm
+            if (typeof d3.beeswarm !== 'undefined') {
+                observations = d3.beeswarm()
+                    .data(g.map(function(e) { return getY(e); }))
+                    .radius(pointSize+1)
+                    .orientation('vertical')
+                    .side('symmetric')
+                    .distributeOn(function(e) { return yScale(e); })
+                    .arrange()
+
+                // add group info for tooltip
+                observations.map(function(e,i) {
+                    e.key = xGroup;
+                    e.object_constancy = g[i].object_constancy;
+                    e.isOutlier = (e.datum < wl.iqr || e.datum > wu.iqr) // add isOulier meta for proper class assignment
+                    e.isOutlierStdDev = (e.datum < wl.stddev || e.datum > wu.stddev) // add isOulier meta for proper class assignment
+                    e.randX = Math.random() * jitter * (Math.floor(Math.random()*2) == 1 ? 1 : -1) // calculate random x-position only once for each point
+                })
+            } else {
+                v.forEach(function(e,i) {
+                    observations.push({
+                        object_constancy: e.object_constancy,
+                        datum: e,
+                        key: xGroup,
+                        isOutlier: (e < wl.iqr || e > wu.iqr), // add isOulier meta for proper class assignment
+                        isOutlierStdDev: (e < wl.stddev || e > wu.stddev), // add isOulier meta for proper class assignment
+                        randX: Math.random() * jitter * (Math.floor(Math.random()*2) == 1 ? 1 : -1)
+                    })
+                })
+            }
+
+
+            // calculate bandwidth if no number is provided
+            if(isNaN(parseFloat(bandwidth))) { // if not is float
+                var bandwidthCalc;
+                if (['scott','silverman'].indexOf(bandwidth) != -1) {
+                    bandwidthCalc = calcBandwidth(v, bandwidth);
+                } else {
+                    bandwidthCalc = calcBandwidth(v); // calculate with default 'scott'
+                }
+            }
+            var kde = kernelDensityEstimator(eKernel(bandwidthCalc), yScale.ticks(resolution));
+            var kdeDat = clampViolin ? clampViolinKDE(kde(v), d3.extent(v)) : kde(v);
+
+
+            // make a new vertical scale for each group
+            var tmpScale = d3.scale.linear()
+                .domain([0, d3.max(kdeDat, function (e) { return e.y;})])
+                .clamp(true);
+            yVScale.push(tmpScale);
+
+            var reformat = {
+                count: v.length,
+                num_outlier: observations.filter(function (e) { return e.isOutlier; }).length,
+                sum: d3.sum(v),
+                mean: mean,
+                q1: q1,
+                q2: median,
+                q3: q3,
+                wl: wl,
+                wu: wu,
+                iqr: iqr,
+                min: d3.min(v),
+                max: d3.max(v),
+                dev: d3.deviation(v),
+                observations: observations,
+                key: xGroup,
+                kde: kdeDat,
+                notch: 1.57 * iqr / Math.sqrt(v.length), // notch distance from mean/median
+            };
+
+            if (colorGroup) {reformatDatFlat.push({key: xGroup, values: reformat});}
+
+            return reformat;
+        }
+
+        // assign a unique identifier for each point for object constancy
+        // this makes updating data possible
+        dat.forEach(function(d,i) { d.object_constancy = i + '_' + getY(d) + '_' + getX(d); })
+
+
+        // TODO not DRY
+        // couldn't find a conditional way of doing the key() grouping
+        var formatted;
+        if (!colorGroup) {
+            formatted = d3.nest()
+                .key(function(d) { return getX(d); })
+                .rollup(function(v,i) {
+                    return calcStats(v);
+                })
+                .entries(dat);
+        } else {
+            allColorGroups = d3.set() // reset
+            var tmp = d3.nest()
+                .key(function(d) { return getX(d); })
+                .key(function(d) { return colorGroup(d); })
+                .rollup(function(v) {
+                    return calcStats(v, getX(v[0]));
+                })
+                .entries(dat);
+
+            // generate a final list of all x & colorGroup combinations
+            // this is used to properly set the x-axis domain
+            allColorGroups = allColorGroups.values(); // convert from d3.set to list
+            var xGroups = tmp.map(function(d) { return d.key; });
+            var allGroups = [];
+            for (var i = 0; i < xGroups.length; i++) {
+                for (var j = 0; j < allColorGroups.length; j++) {
+                    allGroups.push(xGroups[i] + '_' + allColorGroups[j]);
+                }
+            }
+            allColorGroups = allGroups;
+
+            // flatten the inner most level so that
+            // the plot retains the same DOM structure
+            // to allow for smooth updating between
+            // all groups.
+            formatted = [];
+            tmp.forEach(function(d) {
+                d.values.forEach(function(e) { e.key = d.key +'_'+e.key }) // generate a combo key so that each boxplot has a distinct x-position
+                formatted.push.apply(formatted, d.values)
+            });
+
+        }
+        return formatted;
+    }
+
+    // https://bl.ocks.org/mbostock/4341954
+    function kernelDensityEstimator(kernel, X) {
+        return function (sample) {
+            return X.map(function(x) {
+                var y = d3.mean(sample, function (v) {return kernel(x - v);});
+                return {x:x, y:y};
+            });
+        };
+    }
+
+    /*
+     * Limit whether the density extends past the extreme datapoints
+     * of the violin.
+     *
+     * @param (list) kde - x & y kde cooridinates
+     * @param (list) extent - min/max y-values used for clamping violing
+     */
+    function clampViolinKDE(kde, extent) {
+
+        // this handles the case when all the x-values are equal
+        // which means no kde could be properly calculated
+        // just return the kde data so we can continue plotting successfully
+        if (extent[0] === extent[1]) return kde;
+
+        var clamped = kde.reduce(function(res, d) {
+            if (d.x >= extent[0] && d.x <= extent[1]) res.push(d);
+            return res;
+        },[]);
+
+        // add the extreme data points back in
+        if (extent[0] < clamped[0].x) clamped.unshift({x:extent[0], y:clamped[0].y})
+        if (extent[1] > clamped[clamped.length-1].x) clamped.push({x:extent[1], y:clamped[clamped.length-1].y})
+
+        return clamped;
+
+    }
+
+    // https://bl.ocks.org/mbostock/4341954
+    function eKernel(scale) {
+        return function (u) {
+            return Math.abs(u /= scale) <= 1 ? .75 * (1 - u * u) / scale : 0;
+        };
+    }
+
+    /**
+     * Makes the svg polygon string for a boxplot in either a notched
+     * or square version
+     *
+     * NOTE: this actually only draws the left half of the box, since
+     * the shape is symmetric (and since this is how violins are drawn)
+     * we can simply generate half the box and mirror it.
+     *
+     * @param boxLeft {float} - left position of box
+     * @param notchLeft {float} - left position of notch
+     * @param dat {obj} - box plot data that was run through prepDat, must contain
+     *      data for Q1, median, Q2, notch upper and notch lower
+     * @returns {string} A string in the proper format for a svg polygon
+     */
+    function makeNotchBox(boxLeft, notchLeft, boxCenter, dat) {
+
+        var boxPoints;
+        var y = centralTendency == 'mean' ? getMean(dat) : getQ2(dat); // if centralTendency is not specified, we still want to notch boxes on 'median'
+        if (notchBox) {
+            boxPoints = [
+                    {x:boxCenter, y:yScale(getQ1(dat))},
+                    {x:boxLeft, y:yScale(getQ1(dat))},
+                    {x:boxLeft, y:yScale(getNl(dat))},
+                    {x:notchLeft, y:yScale(y)},
+                    {x:boxLeft, y:yScale(getNu(dat))},
+                    {x:boxLeft, y:yScale(getQ3(dat))},
+                    {x:boxCenter, y:yScale(getQ3(dat))},
+                ];
+        } else {
+            boxPoints = [
+                    {x:boxCenter, y:yScale(getQ1(dat))},
+                    {x:boxLeft, y:yScale(getQ1(dat))},
+                    {x:boxLeft, y:yScale(y)}, // repeated point so that transition between notched/regular more smooth
+                    {x:boxLeft, y:yScale(y)},
+                    {x:boxLeft, y:yScale(y)}, // repeated point so that transition between notched/regular more smooth
+                    {x:boxLeft, y:yScale(getQ3(dat))},
+                    {x:boxCenter, y:yScale(getQ3(dat))},
+                ];
+        }
+
+        return boxPoints;
+    }
+
+    /**
+     * Given an x-axis group, return the available color groups within it
+     * provided that colorGroups is set, if not, x-axis group is returned
+     */
+    function getAvailableColorGroups(x) {
+        if (!colorGroup) return x;
+        var tmp = reformatDat.find(function(d) { return d.key == x });
+        return tmp.values.map(function(d) { return d.key }).sort(d3.ascending);
+    }
+
+    // return true if point is an outlier
+    function isOutlier(d) {
+        return (whiskerDef == 'iqr' && d.isOutlier) || (whiskerDef == 'stddev' && d.isOutlierStdDev)
+    }
+
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var allColorGroups = d3.set()
+    var yVScale = [], reformatDat, reformatDatFlat = [];
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+    var availableWidth, availableHeight;
+
+
+    function chart(selection) {
+        renderWatch.reset();
+        selection.each(function(data) {
+            availableWidth = width - margin.left - margin.right,
+            availableHeight = height - margin.top - margin.bottom;
+
+            container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            // Setup y-scale so that beeswarm layout can use it in prepData()
+            yScale.domain(yDomain || d3.extent(data.map(function(d) { return getY(d)}))).nice()
+                .range(yRange || [availableHeight, 0]);
+
+
+            if (typeof reformatDat === 'undefined') reformatDat = prepData(data); // this prevents us from recalculating data all the time
+
+            // Setup x-scale
+            xScale.rangeBands(xRange || [0, availableWidth], 0.1)
+                  .domain(xDomain || (colorGroup && !squash) ? allColorGroups : reformatDat.map(function(d) { return d.key }))
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap').data([reformatDat]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap');
+            wrap.watchTransition(renderWatch, 'nv-wrap: wrap')
+                .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            var areaEnter,
+                distroplots = wrap.selectAll('.nv-distroplot-x-group')
+                    .data(function(d) { return d; });
+
+            // rebind new data
+            // we don't rebuild individual x-axis groups so that we can update transition them
+            // however the data associated with each x-axis group needs to be updated
+            // so we manually update it here
+            distroplots.each(function(d,i) {
+                d3.select(this).selectAll('line.nv-distroplot-middle').datum(d);
+            })
+
+            areaEnter = distroplots.enter()
+                .append('g')
+                .attr('class', 'nv-distroplot-x-group')
+                .style('stroke-opacity', 1e-6).style('fill-opacity', 1e-6)
+                .style('fill', function(d,i) { return getColor(d) || color(d,i) })
+                .style('stroke', function(d,i) { return getColor(d) || color(d,i) })
+
+            distroplots.exit().remove();
+
+            var rangeBand = function() { return xScale.rangeBand() };
+            var areaWidth = function() { return d3.min([maxBoxWidth,rangeBand() * 0.9]); };
+            var areaCenter = function() { return areaWidth()/2; };
+            var areaLeft  = function() { return areaCenter() - areaWidth()/2; };
+            var areaRight = function() { return areaCenter() + areaWidth()/2; };
+            var tickLeft  = function() { return areaCenter() - areaWidth()/5; };
+            var tickRight = function() { return areaCenter() + areaWidth()/5; };
+
+            areaEnter.attr('transform', function(d) {
+                    return 'translate(' + (xScale(d.key) + (rangeBand() - areaWidth()) * 0.5) + ', 0)';
+                });
+
+            distroplots
+                .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots')
+                .style('stroke-opacity', 1)
+                .style('fill-opacity', 0.5)
+                .attr('transform', function(d) {
+                    return 'translate(' + (xScale(d.key) + (rangeBand() - areaWidth()) * 0.5) + ', 0)';
+                });
+
+            // set range for violin scale
+            yVScale.map(function(d) { d.range([areaWidth()/2, 0]) });
+
+            // ----- add the SVG elements for each plot type -----
+
+            // scatter plot type
+            if (!plotType) {
+                showOnlyOutliers = false; // force all observations to be seen
+                if (!observationType) observationType = 'random'
+            }
+
+            // conditionally append whisker lines
+            areaEnter.each(function(d,i) {
+                var box = d3.select(this);
+                [getWl, getWh].forEach(function (f) {
+                    var key = (f === getWl) ? 'low' : 'high';
+                    box.append('line')
+                      .style('opacity', function() { return !hideWhiskers ? '0' : '1' })
+                      .attr('class', 'nv-distroplot-whisker nv-distroplot-' + key)
+                    box.append('line')
+                      .style('opacity', function() { return hideWhiskers ? '0' : '1' })
+                      .attr('class', 'nv-distroplot-tick nv-distroplot-' + key)
+                });
+            });
+
+
+            // update whisker lines and ticks
+            [getWl, getWh].forEach(function (f) {
+                var key = (f === getWl) ? 'low' : 'high';
+                var endpoint = (f === getWl) ? getQ1 : getQ3;
+                distroplots.select('line.nv-distroplot-whisker.nv-distroplot-' + key)
+                  .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots')
+                    .attr('x1', areaCenter())
+                    .attr('y1', function(d) { return plotType!='violin' ? yScale(f(d)) : yScale(getQ2(d)); })
+                    .attr('x2', areaCenter())
+                    .attr('y2', function(d) { return plotType=='box' ? yScale(endpoint(d)) : yScale(getQ2(d)); })
+                    .style('opacity', function() { return hideWhiskers ? '0' : '1' })
+                distroplots.select('line.nv-distroplot-tick.nv-distroplot-' + key)
+                  .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots')
+                    .attr('x1', function(d) { return plotType!='violin' ? tickLeft() : areaCenter()} )
+                    .attr('y1', function(d,i) { return plotType!='violin' ? yScale(f(d)) : yScale(getQ2(d)); })
+                    .attr('x2', function(d) { return plotType!='violin' ? tickRight() : areaCenter()} )
+                    .attr('y2', function(d,i) { return plotType!='violin' ? yScale(f(d)) : yScale(getQ2(d)); })
+                    .style('opacity', function() { return hideWhiskers ? '0' : '1' })
+            });
+
+            [getWl, getWh].forEach(function (f) {
+                var key = (f === getWl) ? 'low' : 'high';
+                areaEnter.selectAll('.nv-distroplot-' + key)
+                  .on('mouseover', function(d,i,j) {
+                      d3.select(this.parentNode).selectAll('line.nv-distroplot-'+key).classed('hover',true);
+                      dispatch.elementMouseover({
+                          value: key == 'low' ? 'Lower whisker' : 'Upper whisker',
+                          series: { key: f(d).toFixed(2), color: getColor(d) || color(d,j) },
+                          e: d3.event
+                      });
+                  })
+                  .on('mouseout', function(d,i,j) {
+                      d3.select(this.parentNode).selectAll('line.nv-distroplot-'+key).classed('hover',false);
+                      dispatch.elementMouseout({
+                          value: key == 'low' ? 'Lower whisker' : 'Upper whisker',
+                          series: { key: f(d).toFixed(2), color: getColor(d) || color(d,j) },
+                          e: d3.event
+                      });
+                  })
+                  .on('mousemove', function(d,i) {
+                      dispatch.elementMousemove({e: d3.event});
+                  });
+            });
+
+            // setup boxes as 4 parts: left-area, left-line, right-area, right-line,
+            // this way we can transition to a violin
+            areaEnter.each(function(d,i) {
+                var violin = d3.select(this);
+
+                ['left','right'].forEach(function(side) {
+                    ['line','area'].forEach(function(d) {
+                        violin.append('path')
+                            .attr('class', 'nv-distribution-' + d + ' nv-distribution-' + side)
+                            .attr("transform", "rotate(90,0,0)   translate(0," + (side == 'left' ? -areaWidth() : 0) + ")" + (side == 'left' ? '' : ' scale(1,-1)')); // rotate violin
+                    })
+
+                })
+
+                areaEnter.selectAll('.nv-distribution-line')
+                    .style('fill','none')
+                areaEnter.selectAll('.nv-distribution-area')
+                    .style('stroke','none')
+                    .style('opacity',0.7)
+
+            });
+
+            // transitions
+            distroplots.each(function(d,i) {
+                var violin = d3.select(this);
+                var objData = plotType == 'box' ? makeNotchBox(areaLeft(), tickLeft(), areaCenter(), d) : d.values.kde;
+
+                violin.selectAll('path')
+                    .datum(objData)
+
+                var tmpScale = yVScale[i];
+
+                var interp = plotType=='box' ? 'linear' : 'basis';
+
+                if (plotType == 'box' || plotType == 'violin') {
+                    ['left','right'].forEach(function(side) {
+
+                        // line
+                        distroplots.selectAll('.nv-distribution-line.nv-distribution-' + side)
+                          //.watchTransition(renderWatch, 'nv-distribution-line: distroplots') // disable transition for now because it's jaring
+                            .attr("d", d3.svg.line()
+                                    .x(function(e) { return plotType=='box' ? e.y : yScale(e.x); })
+                                    .y(function(e) { return plotType=='box' ? e.x : tmpScale(e.y) })
+                                    .interpolate(interp)
+                            )
+                            .attr("transform", "rotate(90,0,0)   translate(0," + (side == 'left' ? -areaWidth() : 0) + ")" + (side == 'left' ? '' : ' scale(1,-1)')) // rotate violin
+                            .style('opacity', !plotType ? '0' : '1');
+
+                        // area
+                        distroplots.selectAll('.nv-distribution-area.nv-distribution-' + side)
+                          //.watchTransition(renderWatch, 'nv-distribution-line: distroplots') // disable transition for now because it's jaring
+                            .attr("d", d3.svg.area()
+                                    .x(function(e) { return plotType=='box' ? e.y : yScale(e.x); })
+                                    .y(function(e) { return plotType=='box' ? e.x : tmpScale(e.y) })
+                                    .y0(areaWidth()/2)
+                                    .interpolate(interp)
+                            )
+                            .attr("transform", "rotate(90,0,0)   translate(0," + (side == 'left' ? -areaWidth() : 0) + ")" + (side == 'left' ? '' : ' scale(1,-1)')) // rotate violin
+                            .style('opacity', !plotType ? '0' : '1');
+
+                    })
+                } else { // scatter type, hide areas
+                    distroplots.selectAll('.nv-distribution-area')
+                        .watchTransition(renderWatch, 'nv-distribution-area: distroplots')
+                        .style('opacity', !plotType ? '0' : '1');
+
+                    distroplots.selectAll('.nv-distribution-line')
+                        .watchTransition(renderWatch, 'nv-distribution-line: distroplots')
+                        .style('opacity', !plotType ? '0' : '1');
+                }
+
+            })
+
+            // tooltip events
+            distroplots.selectAll('path')
+                .on('mouseover', function(d,i,j) {
+                    d = d3.select(this.parentNode).datum(); // grab data from parent g
+                    d3.select(this).classed('hover', true);
+                    dispatch.elementMouseover({
+                        key: d.key,
+                        value: 'Group ' + d.key + ' stats',
+                        series: [
+                            { key: 'max', value: getMax(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'Q3', value: getQ3(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'Q2', value: getQ2(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'Q1', value: getQ1(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'min', value: getMin(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'mean', value: getMean(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'std. dev.', value: getDev(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'count', value: d.values.count, color: getColor(d) || color(d,j) },
+                            { key: 'num. outliers', value: d.values.num_outlier, color: getColor(d) || color(d,j) },
+                        ],
+                        data: d,
+                        index: i,
+                        e: d3.event
+                    });
+                })
+                .on('mouseout', function(d,i,j) {
+                    d3.select(this).classed('hover', false);
+                    d = d3.select(this.parentNode).datum(); // grab data from parent g
+                    dispatch.elementMouseout({
+                        key: d.key,
+                        value: 'Group ' + d.key + ' stats',
+                        series: [
+                            { key: 'max', value: getMax(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'Q3', value: getQ3(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'Q2', value: getQ2(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'Q1', value: getQ1(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'min', value: getMin(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'mean', value: getMean(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'std. dev.', value: getDev(d).toFixed(2), color: getColor(d) || color(d,j) },
+                            { key: 'count', value: d.values.count, color: getColor(d) || color(d,j) },
+                            { key: 'num. outliers', value: d.values.num_outlier, color: getColor(d) || color(d,j) },
+                        ],
+                        data: d,
+                        index: i,
+                        e: d3.event
+                    });
+                })
+                .on('mousemove', function(d,i) {
+                    dispatch.elementMousemove({e: d3.event});
+                });
+
+
+            // median/mean line
+            areaEnter.append('line')
+                .attr('class', function(d) { return 'nv-distroplot-middle'})
+
+
+            distroplots.selectAll('line.nv-distroplot-middle')
+                .watchTransition(renderWatch, 'nv-distroplot-x-group: distroplots line')
+                .attr('x1', notchBox ? tickLeft : plotType != 'violin' ? areaLeft : tickLeft())
+                .attr('y1', function(d,i,j) { return centralTendency == 'mean' ? yScale(getMean(d)) : yScale(getQ2(d)); })
+                .attr('x2', notchBox ? tickRight : plotType != 'violin' ? areaRight : tickRight())
+                .attr('y2', function(d,i) { return centralTendency == 'mean' ? yScale(getMean(d)) : yScale(getQ2(d)); })
+                .style('opacity', centralTendency ? '1' : '0');
+
+
+            // tooltip
+            distroplots.selectAll('.nv-distroplot-middle')
+                .on('mouseover', function(d,i,j) {
+                    if (d3.select(this).style('opacity') == 0) return; // don't show tooltip for hidden lines
+                    var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill
+                    d3.select(this).classed('hover', true);
+                    dispatch.elementMouseover({
+                        value: centralTendency == 'mean' ? 'Mean' : 'Median',
+                        series: { key: centralTendency == 'mean' ? getMean(d).toFixed(2) : getQ2(d).toFixed(2), color: fillColor },
+                        e: d3.event
+                    });
+                })
+                .on('mouseout', function(d,i,j) {
+                    if (d3.select(this).style('opacity') == 0) return; // don't show tooltip for hidden lines
+                    d3.select(this).classed('hover', false);
+                    var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill
+                    dispatch.elementMouseout({
+                        value: centralTendency == 'mean' ? 'Mean' : 'Median',
+                        series: { key: centralTendency == 'mean' ? getMean(d).toFixed(2) : getQ2(d).toFixed(2), color: fillColor },
+                        e: d3.event
+                    });
+                })
+                .on('mousemove', function(d,i) {
+                    dispatch.elementMousemove({e: d3.event});
+                });
+
+
+            // setup observations
+            // create DOMs even if not requested (and hide them), so that
+            // we can do transitions on them
+            var obsWrap = distroplots.selectAll('g.nv-distroplot-observation')
+                .data(function(d) { return getValsObj(d) }, function(d) {  return d.object_constancy; });
+
+            var obsGroup = obsWrap.enter()
+                .append('g')
+                .attr('class', 'nv-distroplot-observation')
+
+            obsGroup.append('circle')
+                .style({'opacity': 0})
+
+            obsGroup.append('line')
+                .style('stroke-width', 1)
+                .style({'stroke': d3.rgb(85, 85, 85), 'opacity': 0})
+
+            obsWrap.exit().remove();
+            obsWrap.attr('class', function(d) { return 'nv-distroplot-observation ' + (isOutlier(d) && plotType == 'box' ? 'nv-distroplot-outlier' : 'nv-distroplot-non-outlier')})
+
+            // transition observations
+            if (observationType == 'line') {
+                distroplots.selectAll('g.nv-distroplot-observation line')
+                  .watchTransition(renderWatch, 'nv-distrolot-x-group: nv-distoplot-observation')
+                    .attr("x1", tickLeft() + areaWidth()/4)
+                    .attr("x2", tickRight() - areaWidth()/4)
+                    .attr('y1', function(d) { return yScale(d.datum)})
+                    .attr('y2', function(d) { return yScale(d.datum)});
+            } else {
+                distroplots.selectAll('g.nv-distroplot-observation circle')
+                  .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation')
+                    .attr('cy', function(d) { return yScale(d.datum); })
+                    .attr('r', pointSize);
+
+                // NOTE: this update can be slow when re-sizing window when many point visible 
+                // TODO: filter selection down to only visible points, no need to update x-position
+                //       of the hidden points
+                distroplots.selectAll('g.nv-distroplot-observation circle')
+                  .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation')
+                    .attr('cx', function(d) { return observationType == 'swarm' ? d.x + areaWidth()/2 : observationType == 'random' ? areaWidth()/2 + d.randX * areaWidth()/2 : areaWidth()/2; })
+
+            }
+
+            // set opacity on outliers/non-outliers
+            // any circle/line entering has opacity 0
+            if (observationType !== false) { // observationType is False when hidding all circle/lines
+                if (!showOnlyOutliers) { // show all line/circle
+                    distroplots.selectAll(observationType== 'line' ? 'line':'circle')
+                      .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation')
+                        .style('opacity',1)
+                } else { // show only outliers
+                    distroplots.selectAll('.nv-distroplot-outlier '+ (observationType== 'line' ? 'line':'circle'))
+                      .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation')
+                        .style('opacity',1)
+                    distroplots.selectAll('.nv-distroplot-non-outlier '+ (observationType== 'line' ? 'line':'circle'))
+                      .watchTransition(renderWatch, 'nv-distroplot: nv-distroplot-observation')
+                        .style('opacity',0)
+                }
+            }
+
+            // hide all other observations
+            distroplots.selectAll('.nv-distroplot-observation' + (observationType=='line'?' circle':' line'))
+              .watchTransition(renderWatch, 'nv-distroplot: nv-distoplot-observation')
+                .style('opacity',0)
+
+            // tooltip events for observations
+            distroplots.selectAll('.nv-distroplot-observation')
+                    .on('mouseover', function(d,i,j) {
+                        var pt = d3.select(this);
+                        if (showOnlyOutliers && plotType == 'box' && !isOutlier(d)) return; // don't show tooltip for hidden observation
+                        var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill
+                        pt.classed('hover', true);
+                        dispatch.elementMouseover({
+                            value: (plotType == 'box' && isOutlier(d)) ? 'Outlier' : 'Observation',
+                            series: { key: d.datum.toFixed(2), color: fillColor },
+                            e: d3.event
+                        });
+                    })
+                    .on('mouseout', function(d,i,j) {
+                        var pt = d3.select(this);
+                        var fillColor = d3.select(this.parentNode).style('fill'); // color set by parent g fill
+                        pt.classed('hover', false);
+                        dispatch.elementMouseout({
+                            value: (plotType == 'box' && isOutlier(d)) ? 'Outlier' : 'Observation',
+                            series: { key: d.datum.toFixed(2), color: fillColor },
+                            e: d3.event
+                        });
+                    })
+                    .on('mousemove', function(d,i) {
+                        dispatch.elementMousemove({e: d3.event});
+                    });
+
+        });
+
+        renderWatch.renderEnd('nv-distroplot-x-group immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:            {get: function(){return width;}, set: function(_){width=_;}},
+        height:           {get: function(){return height;}, set: function(_){height=_;}},
+        maxBoxWidth:      {get: function(){return maxBoxWidth;}, set: function(_){maxBoxWidth=_;}},
+        x:                {get: function(){return getX;}, set: function(_){getX=_;}},
+        y:                {get: function(){return getY;}, set: function(_){getY=_;}},
+        plotType:         {get: function(){return plotType;}, set: function(_){plotType=_;}}, // plotType of background: 'box', 'violin' - default: 'box'
+        observationType:  {get: function(){return observationType;}, set: function(_){observationType=_;}}, // type of observations to show: 'random', 'swarm', 'line', 'point' - default: false (don't show observations)
+        whiskerDef:       {get: function(){return whiskerDef;}, set: function(_){whiskerDef=_;}}, // type of whisker to render: 'iqr', 'minmax', 'stddev' - default: iqr
+        notchBox:         {get: function(){return notchBox;}, set: function(_){notchBox=_;}}, // bool whether to notch box
+        hideWhiskers:     {get: function(){return hideWhiskers;}, set: function(_){hideWhiskers=_;}},
+        colorGroup:       {get: function(){return colorGroup;}, set: function(_){colorGroup=_;}}, // data key to use to set color group of each x-category - default: don't group
+        centralTendency:       {get: function(){return centralTendency;}, set: function(_){centralTendency=_;}}, // add a mean or median line to the data - default: don't show, must be one of 'mean' or 'median'
+        bandwidth:        {get: function(){return bandwidth;}, set: function(_){bandwidth=_;}}, // bandwidth for kde calculation, can be float or str, if str, must be one of scott or silverman
+        clampViolin:           {get: function(){return clampViolin;}, set: function(_){clampViolin=_;}},
+        resolution:       {get: function(){return resolution;}, set: function(_){resolution=_;}}, // resolution for kde calculation, default 50
+        xScale:           {get: function(){return xScale;}, set: function(_){xScale=_;}},
+        yScale:           {get: function(){return yScale;}, set: function(_){yScale=_;}},
+        showOnlyOutliers: {get: function(){return showOnlyOutliers;}, set: function(_){showOnlyOutliers=_;}}, // show only outliers in box plot, default true
+        jitter:           {get: function(){return jitter;}, set: function(_){jitter=_;}}, // faction of that jitter should take up in 'random' observationType, must be in range [0,1]; see jitterX(), default 0.7
+        squash:           {get: function(){return squash;}, set: function(_){squash=_;}}, // whether to squash sparse distribution of color groups towards middle of x-axis position
+        pointSize:     {get: function(){return pointSize;}, set: function(_){pointSize=_;}},
+        xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+        yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+        xRange:  {get: function(){return xRange;}, set: function(_){xRange=_;}},
+        yRange:  {get: function(){return yRange;}, set: function(_){yRange=_;}},
+        recalcData:   {get: function() { reformatDat = prepData(container.datum()); } },
+        itemColor:    {get: function(){return getColor;}, set: function(_){getColor=_;}},
+        id:           {get: function(){return id;}, set: function(_){id=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+nv.models.distroPlotChart = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var distroplot = nv.models.distroPlot(),
+        xAxis = nv.models.axis(),
+        yAxis = nv.models.axis()
+
+    var margin = {top: 25, right: 10, bottom: 40, left: 60},
+        width = null,
+        height = null,
+        color = nv.utils.getColor(),
+        showXAxis = true,
+        showYAxis = true,
+        rightAlignYAxis = false,
+        staggerLabels = false,
+        xLabel = false,
+        yLabel = false,
+        tooltip = nv.models.tooltip(),
+        x, y,
+        noData = 'No Data Available.',
+        dispatch = d3.dispatch('stateChange', 'beforeUpdate', 'renderEnd'),
+        duration = 500;
+
+    xAxis
+        .orient('bottom')
+        .showMaxMin(false)
+        .tickFormat(function(d) { return d })
+    ;
+    yAxis
+        .orient((rightAlignYAxis) ? 'right' : 'left')
+        .tickFormat(d3.format(',.1f'))
+    ;
+
+    tooltip.duration(0);
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+    var colorGroup0, marginTop0 = margin.top, x0, y0, resolution0, bandwidth0, clampViolin0;
+    var dataCache;
+
+
+    // return true if data has changed somehow after
+    // an .update() was called
+    // works by comparing current data set to the
+    // one previously cached
+    // TODO - since we keep another version of the dataset
+    // around for comparison, it doubles the memory usage :(
+    function dataHasChanged(d) {
+        if (arraysEqual(d, dataCache)) {
+            return false;
+        } else {
+            dataCache = JSON.parse(JSON.stringify(d)) // deep copy
+            return true;
+        }
+    }
+
+    // return true if array of objects equivalent
+    function arraysEqual(arr1, arr2) {
+        if(arr1.length !== arr2.length) return false;
+
+        for(var i = arr1.length; i--;) {
+            if ('object_constancy' in arr1[i]) delete arr1[i].object_constancy
+            if ('object_constancy' in arr2[i]) delete arr2[i].object_constancy
+
+            if(!objectEquals(arr1[i], arr2[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // return true if objects are equivalent
+    function objectEquals(a, b) {
+        // Create arrays of property names
+        var aProps = Object.getOwnPropertyNames(a);
+        var bProps = Object.getOwnPropertyNames(b);
+
+        // If number of properties is different,
+        // objects are not equivalent
+        if (aProps.length != bProps.length) {
+            return false;
+        }
+
+        for (var i = 0; i < aProps.length; i++) {
+            var propName = aProps[i];
+
+            // If values of same property are not equal,
+            // objects are not equivalent
+            if (a[propName] !== b[propName]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
+    function chart(selection) {
+        renderWatch.reset();
+        renderWatch.models(distroplot);
+        if (showXAxis) renderWatch.models(xAxis);
+        if (showYAxis) renderWatch.models(yAxis);
+
+        selection.each(function(data) {
+            var container = d3.select(this), that = this;
+            nv.utils.initSVG(container);
+            var availableWidth = (width  || parseInt(container.style('width')) || 960) - margin.left - margin.right;
+            var availableHeight = (height || parseInt(container.style('height')) || 400) - margin.top - margin.bottom;
+
+            if (typeof dataCache === 'undefined') {
+                dataCache = JSON.parse(JSON.stringify(data)) // deep copy
+            }
+
+            chart.update = function() {
+                dispatch.beforeUpdate();
+                var opts = distroplot.options()
+                if (colorGroup0 !== opts.colorGroup() || // recalc data when any of the axis accessors are changed
+                    x0 !== opts.x() ||
+                    y0 !== opts.y() ||
+                    bandwidth0 !== opts.bandwidth() ||
+                    resolution0 !== opts.resolution() ||
+                    clampViolin0 !== opts.clampViolin() ||
+                    dataHasChanged(data)
+                ) {
+                    distroplot.recalcData();
+                }
+                container.transition().duration(duration).call(chart);
+            };
+            chart.container = this;
+
+
+            if (typeof d3.beeswarm !== 'function' && chart.options().observationType() == 'swarm') {
+                var xPos = margin.left + availableWidth/2;
+                noData = 'Please include the library https://github.com/Kcnarf/d3-beeswarm to use "swarm".'
+                nv.utils.noData(chart, container);
+                return chart;
+            } else if (!data || !data.length) {
+                nv.utils.noData(chart, container);
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            // Setup Scales
+            x = distroplot.xScale();
+            y = distroplot.yScale().clamp(true);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-distroPlot').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-distroPlot').append('g');
+            var defsEnter = gEnter.append('defs');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-x nv-axis');
+            gEnter.append('g').attr('class', 'nv-y nv-axis')
+                .append('g').attr('class', 'nv-zeroLine')
+                .append('line');
+
+            gEnter.append('g').attr('class', 'nv-distroWrap');
+            gEnter.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+            g.watchTransition(renderWatch, 'nv-wrap: wrap')
+                .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            if (rightAlignYAxis) {
+                g.select('.nv-y.nv-axis')
+                    .attr('transform', 'translate(' + availableWidth + ',0)');
+            }
+
+
+            // Main Chart Component(s)
+            distroplot.width(availableWidth).height(availableHeight);
+
+            var distroWrap = g.select('.nv-distroWrap')
+                .datum(data)
+
+            distroWrap.transition().call(distroplot);
+
+            defsEnter.append('clipPath')
+                .attr('id', 'nv-x-label-clip-' + distroplot.id())
+                .append('rect');
+
+            g.select('#nv-x-label-clip-' + distroplot.id() + ' rect')
+                .attr('width', x.rangeBand() * (staggerLabels ? 2 : 1))
+                .attr('height', 16)
+                .attr('x', -x.rangeBand() / (staggerLabels ? 1 : 2 ));
+
+            // Setup Axes
+            if (showXAxis) {
+                xAxis
+                    .scale(x)
+                    .ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+                    .tickSize(-availableHeight, 0);
+
+                g.select('.nv-x.nv-axis').attr('transform', 'translate(0,' + y.range()[0] + ')')
+                g.select('.nv-x.nv-axis').call(xAxis);
+
+                //g.select('.nv-x.nv-axis').select('.nv-axislabel')
+                //    .style('font-size', d3.min([availableWidth * 0.05,20]) + 'px')
+
+                var xTicks = g.select('.nv-x.nv-axis').selectAll('g');
+                if (staggerLabels) {
+                    xTicks
+                        .selectAll('text')
+                        .attr('transform', function(d,i,j) { return 'translate(0,' + (j % 2 === 0 ? '5' : '17') + ')' })
+                }
+            }
+
+            if (showYAxis) {
+                yAxis
+                    .scale(y)
+                    .ticks( Math.floor(availableHeight/36) ) // can't use nv.utils.calcTicksY with Object data
+                    .tickSize( -availableWidth, 0);
+
+                g.select('.nv-y.nv-axis').call(yAxis);
+
+                //g.select('.nv-y.nv-axis').select('.nv-axislabel')
+                //    .style('font-size', d3.min([availableHeight * 0.05,20]) + 'px')
+            }
+
+
+
+
+            // Zero line on chart bottom
+            g.select('.nv-zeroLine line')
+                .attr('x1',0)
+                .attr('x2',availableWidth)
+                .attr('y1', y(0))
+                .attr('y2', y(0))
+            ;
+
+            // store original values so that we can
+            // call 'recalcData()' if needed
+            colorGroup0 = distroplot.options().colorGroup();
+            x0 = distroplot.options().x();
+            y0 = distroplot.options().y();
+            bandwidth0 = distroplot.options().bandwidth();
+            resolution0 = distroplot.options().resolution();
+            clampViolin0 = distroplot.options().clampViolin();
+
+            //============================================================
+            // Event Handling/Dispatching (in chart's scope)
+            //------------------------------------------------------------
+
+        });
+
+        renderWatch.renderEnd('nv-distroplot chart immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    distroplot.dispatch.on('elementMouseover.tooltip', function(evt) {
+        tooltip.data(evt).hidden(false);
+    });
+
+    distroplot.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.data(evt).hidden(true);
+    });
+
+    distroplot.dispatch.on('elementMousemove.tooltip', function(evt) {
+        tooltip();
+    });
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.distroplot = distroplot;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+        staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+        showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        tooltipContent:    {get: function(){return tooltip;}, set: function(_){tooltip=_;}},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+        defaultState:    {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+            distroplot.duration(duration);
+            xAxis.duration(duration);
+            yAxis.duration(duration);
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+            distroplot.color(color);
+        }},
+        rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+            rightAlignYAxis = _;
+            yAxis.orient( (_) ? 'right' : 'left');
+        }},
+        xLabel:  {get: function(){return xLabel;}, set: function(_){
+            xLabel=_;
+            xAxis.axisLabel(xLabel);
+        }},
+        yLabel:  {get: function(){return yLabel;}, set: function(_){
+            yLabel=_;
+            yAxis.axisLabel(yLabel);
+        }},
+    });
+
+
+    nv.utils.inheritOptions(chart, distroplot);
+    nv.utils.initOptions(chart);
+
+    return chart;
+}
+nv.models.focus = function(content) {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var content = content || nv.models.line()
+        , xAxis = nv.models.axis()
+        , yAxis = nv.models.axis()
+        , brush = d3.svg.brush()
+        ;
+
+    var margin = {top: 10, right: 0, bottom: 30, left: 0}
+        , color = nv.utils.defaultColor()
+        , width = null
+        , height = 70
+        , showXAxis = true
+        , showYAxis = false
+        , rightAlignYAxis = false
+        , ticks = null
+        , x
+        , y
+        , brushExtent = null
+        , duration = 250
+        , dispatch = d3.dispatch('brush', 'onBrush', 'renderEnd')
+        , syncBrushing = true
+        ;
+
+    content.interactive(false);
+    content.pointActive(function(d) { return false; });
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        renderWatch.models(content);
+        if (showXAxis) renderWatch.models(xAxis);
+        if (showYAxis) renderWatch.models(yAxis);
+
+        selection.each(function(data) {
+            var container = d3.select(this);
+            nv.utils.initSVG(container);
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = height - margin.top - margin.bottom;
+
+            chart.update = function() { 
+                if( duration === 0 ) {
+                    container.call( chart );
+                } else {
+                    container.transition().duration(duration).call(chart);
+                }
+            };
+            chart.container = this;
+
+            // Setup Scales
+            x = content.xScale();
+            y = content.yScale();
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-focus').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-focus').append('g');
+            var g = wrap.select('g');
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            gEnter.append('g').attr('class', 'nv-background').append('rect');
+            gEnter.append('g').attr('class', 'nv-x nv-axis');
+            gEnter.append('g').attr('class', 'nv-y nv-axis');
+            gEnter.append('g').attr('class', 'nv-contentWrap');
+            gEnter.append('g').attr('class', 'nv-brushBackground');
+            gEnter.append('g').attr('class', 'nv-x nv-brush');
+
+            if (rightAlignYAxis) {
+                g.select(".nv-y.nv-axis")
+                    .attr("transform", "translate(" + availableWidth + ",0)");
+            }
+
+            g.select('.nv-background rect')
+                .attr('width', availableWidth)
+                .attr('height', availableHeight);
+                
+            content
+                .width(availableWidth)
+                .height(availableHeight)
+                .color(data.map(function(d,i) {
+                    return d.color || color(d, i);
+                }).filter(function(d,i) { return !data[i].disabled; }));
+
+            var contentWrap = g.select('.nv-contentWrap')
+                .datum(data.filter(function(d) { return !d.disabled; }));
+
+            d3.transition(contentWrap).call(content);
+            
+            // Setup Brush
+            brush
+                .x(x)
+                .on('brush', function() {
+                    onBrush(syncBrushing);
+                });
+
+            brush.on('brushend', function () {
+                if (!syncBrushing) {
+                    dispatch.onBrush(brush.empty() ? x.domain() : brush.extent());
+                }
+            });
+
+            if (brushExtent) brush.extent(brushExtent);
+
+            var brushBG = g.select('.nv-brushBackground').selectAll('g')
+                .data([brushExtent || brush.extent()]);
+    
+            var brushBGenter = brushBG.enter()
+                .append('g');
+
+            brushBGenter.append('rect')
+                .attr('class', 'left')
+                .attr('x', 0)
+                .attr('y', 0)
+                .attr('height', availableHeight);
+
+            brushBGenter.append('rect')
+                .attr('class', 'right')
+                .attr('x', 0)
+                .attr('y', 0)
+                .attr('height', availableHeight);
+
+            var gBrush = g.select('.nv-x.nv-brush')
+                .call(brush);
+            gBrush.selectAll('rect')
+                .attr('height', availableHeight);
+            gBrush.selectAll('.resize').append('path').attr('d', resizePath);
+
+            onBrush(true);
+
+            g.select('.nv-background rect')
+                .attr('width', availableWidth)
+                .attr('height', availableHeight);
+
+            if (showXAxis) {
+                xAxis.scale(x)
+                    ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+                    .tickSize(-availableHeight, 0);
+  
+                g.select('.nv-x.nv-axis')
+                    .attr('transform', 'translate(0,' + y.range()[0] + ')');
+                d3.transition(g.select('.nv-x.nv-axis'))
+                    .call(xAxis);
+            }
+
+            if (showYAxis) {
+                yAxis
+                    .scale(y)
+                    ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+                    .tickSize( -availableWidth, 0);
+
+                d3.transition(g.select('.nv-y.nv-axis'))
+                    .call(yAxis);
+            }
+            
+            g.select('.nv-x.nv-axis')
+                .attr('transform', 'translate(0,' + y.range()[0] + ')');
+
+            //============================================================
+            // Event Handling/Dispatching (in chart's scope)
+            //------------------------------------------------------------
+
+            //============================================================
+            // Functions
+            //------------------------------------------------------------
+    
+            // Taken from crossfilter (http://square.github.com/crossfilter/)
+            function resizePath(d) {
+                var e = +(d == 'e'),
+                    x = e ? 1 : -1,
+                    y = availableHeight / 3;
+                return 'M' + (0.5 * x) + ',' + y
+                    + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6)
+                    + 'V' + (2 * y - 6)
+                    + 'A6,6 0 0 ' + e + ' ' + (0.5 * x) + ',' + (2 * y)
+                    + 'Z'
+                    + 'M' + (2.5 * x) + ',' + (y + 8)
+                    + 'V' + (2 * y - 8)
+                    + 'M' + (4.5 * x) + ',' + (y + 8)
+                    + 'V' + (2 * y - 8);
+            }
+    
+    
+            function updateBrushBG() {
+                if (!brush.empty()) brush.extent(brushExtent);
+                brushBG
+                    .data([brush.empty() ? x.domain() : brushExtent])
+                    .each(function(d,i) {
+                        var leftWidth = x(d[0]) - x.range()[0],
+                            rightWidth = availableWidth - x(d[1]);
+                        d3.select(this).select('.left')
+                            .attr('width',  leftWidth < 0 ? 0 : leftWidth);
+    
+                        d3.select(this).select('.right')
+                            .attr('x', x(d[1]))
+                            .attr('width', rightWidth < 0 ? 0 : rightWidth);
+                    });
+            }
+
+
+            function onBrush(shouldDispatch) {
+                brushExtent = brush.empty() ? null : brush.extent();
+                var extent = brush.empty() ? x.domain() : brush.extent();
+                dispatch.brush({extent: extent, brush: brush});
+                updateBrushBG();
+                if (shouldDispatch) {
+                    dispatch.onBrush(extent);
+                }
+            }
+        });
+
+        renderWatch.renderEnd('focus immediate');
+        return chart;
+    }
+
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    // expose chart's sub-components
+    chart.dispatch = dispatch;
+    chart.content = content;
+    chart.brush = brush;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+        showXAxis:      {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis:    {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}},
+        syncBrushing: {get: function(){return syncBrushing;}, set: function(_){syncBrushing=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+            content.duration(duration);
+            xAxis.duration(duration);
+            yAxis.duration(duration);
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+            content.color(color);
+        }},
+        interpolate: {get: function(){return content.interpolate();}, set: function(_){
+            content.interpolate(_);
+        }},
+        xTickFormat: {get: function(){return xAxis.tickFormat();}, set: function(_){
+            xAxis.tickFormat(_);
+        }},
+        yTickFormat: {get: function(){return yAxis.tickFormat();}, set: function(_){
+            yAxis.tickFormat(_);
+        }},
+        x: {get: function(){return content.x();}, set: function(_){
+            content.x(_);
+        }},
+        y: {get: function(){return content.y();}, set: function(_){
+            content.y(_);
+        }},
+        rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+            rightAlignYAxis = _;
+            yAxis.orient( rightAlignYAxis ? 'right' : 'left');
+        }}
+    });
+
+    nv.utils.inheritOptions(chart, content);
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+nv.models.forceDirectedGraph = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+    var margin = {top: 2, right: 0, bottom: 2, left: 0}
+        , width = 400
+        , height = 32
+        , container = null
+        , dispatch = d3.dispatch('renderEnd')
+        , color = nv.utils.getColor(['#000'])
+        , tooltip      = nv.models.tooltip()
+        , noData = null
+        // Force directed graph specific parameters [default values]
+        , linkStrength = 0.1
+        , friction = 0.9
+        , linkDist = 30
+        , charge = -120
+        , gravity = 0.1
+        , theta = 0.8
+        , alpha = 0.1
+        , radius = 5
+        // These functions allow to add extra attributes to ndes and links
+        ,nodeExtras = function(nodes) { /* Do nothing */ }
+        ,linkExtras = function(links) { /* Do nothing */ }
+        , getX=d3.functor(0.0)
+        , getY=d3.functor(0.0)
+        ;
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var renderWatch = nv.utils.renderWatch(dispatch);
+
+    function chart(selection) {
+        renderWatch.reset();
+
+        selection.each(function(data) {
+          container = d3.select(this);
+          nv.utils.initSVG(container);
+
+          var availableWidth = nv.utils.availableWidth(width, container, margin),
+              availableHeight = nv.utils.availableHeight(height, container, margin);
+
+          container
+                  .attr("width", availableWidth)
+                  .attr("height", availableHeight);
+
+          // Display No Data message if there's nothing to show.
+          if (!data || !data.links || !data.nodes) {
+              nv.utils.noData(chart, container)
+              return chart;
+          } else {
+              container.selectAll('.nv-noData').remove();
+          }
+          container.selectAll('*').remove();
+
+          // Collect names of all fields in the nodes
+          var nodeFieldSet = new Set();
+          data.nodes.forEach(function(node) {
+            var keys = Object.keys(node);
+            keys.forEach(function(key) {
+              nodeFieldSet.add(key);
+            });
+          });
+
+          var force = d3.layout.force()
+                .nodes(data.nodes)
+                .links(data.links)
+                .size([availableWidth, availableHeight])
+                .linkStrength(linkStrength)
+                .friction(friction)
+                .linkDistance(linkDist)
+                .charge(charge)
+                .gravity(gravity)
+                .theta(theta)
+                .alpha(alpha)
+                .start();
+
+          var link = container.selectAll(".link")
+                .data(data.links)
+                .enter().append("line")
+                .attr("class", "nv-force-link")
+                .style("stroke-width", function(d) { return Math.sqrt(d.value); });
+
+          var node = container.selectAll(".node")
+                .data(data.nodes)
+                .enter()
+                .append("g")
+                .attr("class", "nv-force-node")
+                .call(force.drag);
+
+          node
+            .append("circle")
+            .attr("r", radius)
+            .style("fill", function(d) { return color(d) } )
+            .on("mouseover", function(evt) {
+              container.select('.nv-series-' + evt.seriesIndex + ' .nv-distx-' + evt.pointIndex)
+                  .attr('y1', evt.py);
+              container.select('.nv-series-' + evt.seriesIndex + ' .nv-disty-' + evt.pointIndex)
+                  .attr('x2', evt.px);
+
+              // Add 'series' object to
+              var nodeColor = color(evt);
+              evt.series = [];
+              nodeFieldSet.forEach(function(field) {
+                evt.series.push({
+                  color: nodeColor,
+                  key:   field,
+                  value: evt[field]
+                });
+              });
+              tooltip.data(evt).hidden(false);
+            })
+            .on("mouseout",  function(d) {
+              tooltip.hidden(true);
+            });
+
+          tooltip.headerFormatter(function(d) {return "Node";});
+
+          // Apply extra attributes to nodes and links (if any)
+          linkExtras(link);
+          nodeExtras(node);
+
+          force.on("tick", function() {
+              link.attr("x1", function(d) { return d.source.x; })
+                  .attr("y1", function(d) { return d.source.y; })
+                  .attr("x2", function(d) { return d.target.x; })
+                  .attr("y2", function(d) { return d.target.y; });
+
+              node.attr("transform", function(d) {
+                return "translate(" + d.x + ", " + d.y + ")";
+              });
+            });
+        });
+
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:     {get: function(){return width;}, set: function(_){width=_;}},
+        height:    {get: function(){return height;}, set: function(_){height=_;}},
+
+        // Force directed graph specific parameters
+        linkStrength:{get: function(){return linkStrength;}, set: function(_){linkStrength=_;}},
+        friction:    {get: function(){return friction;}, set: function(_){friction=_;}},
+        linkDist:    {get: function(){return linkDist;}, set: function(_){linkDist=_;}},
+        charge:      {get: function(){return charge;}, set: function(_){charge=_;}},
+        gravity:     {get: function(){return gravity;}, set: function(_){gravity=_;}},
+        theta:       {get: function(){return theta;}, set: function(_){theta=_;}},
+        alpha:       {get: function(){return alpha;}, set: function(_){alpha=_;}},
+        radius:      {get: function(){return radius;}, set: function(_){radius=_;}},
+
+        //functor options
+        x: {get: function(){return getX;}, set: function(_){getX=d3.functor(_);}},
+        y: {get: function(){return getY;}, set: function(_){getY=d3.functor(_);}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+        nodeExtras: {get: function(){return nodeExtras;}, set: function(_){
+            nodeExtras = _;
+        }},
+        linkExtras: {get: function(){return linkExtras;}, set: function(_){
+            linkExtras = _;
+        }}
+    });
+
+    chart.dispatch = dispatch;
+    chart.tooltip = tooltip;
+    nv.utils.initOptions(chart);
+    return chart;
+};
+nv.models.furiousLegend = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 5, right: 0, bottom: 5, left: 0}
+        , width = 400
+        , height = 20
+        , getKey = function(d) { return d.key }
+        , keyFormatter = function (d) { return d }
+        , color = nv.utils.getColor()
+        , maxKeyLength = 20 //default value for key lengths
+        , align = true
+        , padding = 28 //define how much space between legend items. - recommend 32 for furious version
+        , rightAlign = true
+        , updateState = true   //If true, legend will update data.disabled and trigger a 'stateChange' dispatch.
+        , radioButtonMode = false   //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time)
+        , expanded = false
+        , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange')
+        , vers = 'classic' //Options are "classic" and "furious"
+        ;
+
+    function chart(selection) {
+        selection.each(function(data) {
+            var availableWidth = width - margin.left - margin.right,
+                container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-legend').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g');
+            var g = wrap.select('g');
+
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            var series = g.selectAll('.nv-series')
+                .data(function(d) {
+                    if(vers != 'furious') return d;
+
+                    return d.filter(function(n) {
+                        return expanded ? true : !n.disengaged;
+                    });
+                });
+            var seriesEnter = series.enter().append('g').attr('class', 'nv-series')
+
+            var seriesShape;
+
+            if(vers == 'classic') {
+                seriesEnter.append('circle')
+                    .style('stroke-width', 2)
+                    .attr('class','nv-legend-symbol')
+                    .attr('r', 5);
+
+                seriesShape = series.select('circle');
+            } else if (vers == 'furious') {
+                seriesEnter.append('rect')
+                    .style('stroke-width', 2)
+                    .attr('class','nv-legend-symbol')
+                    .attr('rx', 3)
+                    .attr('ry', 3);
+
+                seriesShape = series.select('rect');
+
+                seriesEnter.append('g')
+                    .attr('class', 'nv-check-box')
+                    .property('innerHTML','<path d="M0.5,5 L22.5,5 L22.5,26.5 L0.5,26.5 L0.5,5 Z" class="nv-box"></path><path d="M5.5,12.8618467 L11.9185089,19.2803556 L31,0.198864511" class="nv-check"></path>')
+                    .attr('transform', 'translate(-10,-8)scale(0.5)');
+
+                var seriesCheckbox = series.select('.nv-check-box');
+
+                seriesCheckbox.each(function(d,i) {
+                    d3.select(this).selectAll('path')
+                        .attr('stroke', setTextColor(d,i));
+                });
+            }
+
+            seriesEnter.append('text')
+                .attr('text-anchor', 'start')
+                .attr('class','nv-legend-text')
+                .attr('dy', '.32em')
+                .attr('dx', '8');
+
+            var seriesText = series.select('text.nv-legend-text');
+
+            series
+                .on('mouseover', function(d,i) {
+                    dispatch.legendMouseover(d,i);  //TODO: Make consistent with other event objects
+                })
+                .on('mouseout', function(d,i) {
+                    dispatch.legendMouseout(d,i);
+                })
+                .on('click', function(d,i) {
+                    dispatch.legendClick(d,i);
+                    // make sure we re-get data in case it was modified
+                    var data = series.data();
+                    if (updateState) {
+                        if(vers =='classic') {
+                            if (radioButtonMode) {
+                                //Radio button mode: set every series to disabled,
+                                //  and enable the clicked series.
+                                data.forEach(function(series) { series.disabled = true});
+                                d.disabled = false;
+                            }
+                            else {
+                                d.disabled = !d.disabled;
+                                if (data.every(function(series) { return series.disabled})) {
+                                    //the default behavior of NVD3 legends is, if every single series
+                                    // is disabled, turn all series' back on.
+                                    data.forEach(function(series) { series.disabled = false});
+                                }
+                            }
+                        } else if(vers == 'furious') {
+                            if(expanded) {
+                                d.disengaged = !d.disengaged;
+                                d.userDisabled = d.userDisabled == undefined ? !!d.disabled : d.userDisabled;
+                                d.disabled = d.disengaged || d.userDisabled;
+                            } else if (!expanded) {
+                                d.disabled = !d.disabled;
+                                d.userDisabled = d.disabled;
+                                var engaged = data.filter(function(d) { return !d.disengaged; });
+                                if (engaged.every(function(series) { return series.userDisabled })) {
+                                    //the default behavior of NVD3 legends is, if every single series
+                                    // is disabled, turn all series' back on.
+                                    data.forEach(function(series) {
+                                        series.disabled = series.userDisabled = false;
+                                    });
+                                }
+                            }
+                        }
+                        dispatch.stateChange({
+                            disabled: data.map(function(d) { return !!d.disabled }),
+                            disengaged: data.map(function(d) { return !!d.disengaged })
+                        });
+
+                    }
+                })
+                .on('dblclick', function(d,i) {
+                    if(vers == 'furious' && expanded) return;
+                    dispatch.legendDblclick(d,i);
+                    if (updateState) {
+                        // make sure we re-get data in case it was modified
+                        var data = series.data();
+                        //the default behavior of NVD3 legends, when double clicking one,
+                        // is to set all other series' to false, and make the double clicked series enabled.
+                        data.forEach(function(series) {
+                            series.disabled = true;
+                            if(vers == 'furious') series.userDisabled = series.disabled;
+                        });
+                        d.disabled = false;
+                        if(vers == 'furious') d.userDisabled = d.disabled;
+                        dispatch.stateChange({
+                            disabled: data.map(function(d) { return !!d.disabled })
+                        });
+                    }
+                });
+
+            series.classed('nv-disabled', function(d) { return d.userDisabled });
+            series.exit().remove();
+
+            seriesText
+                .attr('fill', setTextColor)
+                .text(function (d) { return keyFormatter(getKey(d)) });
+
+            //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option)
+            // NEW ALIGNING CODE, TODO: clean up
+
+            var versPadding;
+            switch(vers) {
+                case 'furious' :
+                    versPadding = 23;
+                    break;
+                case 'classic' :
+                    versPadding = 20;
+            }
+
+            if (align) {
+
+                var seriesWidths = [];
+                series.each(function(d,i) {
+                    var legendText;
+                    if (keyFormatter(getKey(d)) && keyFormatter(getKey(d)).length > maxKeyLength) {
+                        var trimmedKey = keyFormatter(getKey(d)).substring(0, maxKeyLength);
+                        legendText = d3.select(this).select('text').text(trimmedKey + "...");
+                        d3.select(this).append("svg:title").text(keyFormatter(getKey(d)));
+                    } else {
+                        legendText = d3.select(this).select('text');
+                    }
+                    var nodeTextLength;
+                    try {
+                        nodeTextLength = legendText.node().getComputedTextLength();
+                        // If the legendText is display:none'd (nodeTextLength == 0), simulate an error so we approximate, instead
+                        if(nodeTextLength <= 0) throw Error();
+                    }
+                    catch(e) {
+                        nodeTextLength = nv.utils.calcApproxTextWidth(legendText);
+                    }
+
+                    seriesWidths.push(nodeTextLength + padding);
+                });
+
+                var seriesPerRow = 0;
+                var legendWidth = 0;
+                var columnWidths = [];
+
+                while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) {
+                    columnWidths[seriesPerRow] = seriesWidths[seriesPerRow];
+                    legendWidth += seriesWidths[seriesPerRow++];
+                }
+                if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row
+
+                while ( legendWidth > availableWidth && seriesPerRow > 1 ) {
+                    columnWidths = [];
+                    seriesPerRow--;
+
+                    for (var k = 0; k < seriesWidths.length; k++) {
+                        if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) )
+                            columnWidths[k % seriesPerRow] = seriesWidths[k];
+                    }
+
+                    legendWidth = columnWidths.reduce(function(prev, cur, index, array) {
+                        return prev + cur;
+                    });
+                }
+
+                var xPositions = [];
+                for (var i = 0, curX = 0; i < seriesPerRow; i++) {
+                    xPositions[i] = curX;
+                    curX += columnWidths[i];
+                }
+
+                series
+                    .attr('transform', function(d, i) {
+                        return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * versPadding) + ')';
+                    });
+
+                //position legend as far right as possible within the total width
+                if (rightAlign) {
+                    g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')');
+                }
+                else {
+                    g.attr('transform', 'translate(0' + ',' + margin.top + ')');
+                }
+
+                height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * versPadding);
+
+            } else {
+
+                var ypos = 5,
+                    newxpos = 5,
+                    maxwidth = 0,
+                    xpos;
+                series
+                    .attr('transform', function(d, i) {
+                        var length = d3.select(this).select('text').node().getComputedTextLength() + padding;
+                        xpos = newxpos;
+
+                        if (width < margin.left + margin.right + xpos + length) {
+                            newxpos = xpos = 5;
+                            ypos += versPadding;
+                        }
+
+                        newxpos += length;
+                        if (newxpos > maxwidth) maxwidth = newxpos;
+
+                        return 'translate(' + xpos + ',' + ypos + ')';
+                    });
+
+                //position legend as far right as possible within the total width
+                g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')');
+
+                height = margin.top + margin.bottom + ypos + 15;
+            }
+
+            if(vers == 'furious') {
+                // Size rectangles after text is placed
+                seriesShape
+                    .attr('width', function(d,i) {
+                        return seriesText[0][i].getComputedTextLength() + 27;
+                    })
+                    .attr('height', 18)
+                    .attr('y', -9)
+                    .attr('x', -15)
+            }
+
+            seriesShape
+                .style('fill', setBGColor)
+                .style('stroke', function(d,i) { return d.color || color(d, i) });
+        });
+
+        function setTextColor(d,i) {
+            if(vers != 'furious') return '#000';
+            if(expanded) {
+                return d.disengaged ? color(d,i) : '#fff';
+            } else if (!expanded) {
+                return !!d.disabled ? color(d,i) : '#fff';
+            }
+        }
+
+        function setBGColor(d,i) {
+            if(expanded && vers == 'furious') {
+                return d.disengaged ? '#fff' : color(d,i);
+            } else {
+                return !!d.disabled ? '#fff' : color(d,i);
+            }
+        }
+
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:          {get: function(){return width;}, set: function(_){width=_;}},
+        height:         {get: function(){return height;}, set: function(_){height=_;}},
+        key:            {get: function(){return getKey;}, set: function(_){getKey=_;}},
+        keyFormatter:   {get: function(){return keyFormatter;}, set: function(_){keyFormatter=_;}},
+        align:          {get: function(){return align;}, set: function(_){align=_;}},
+        rightAlign:     {get: function(){return rightAlign;}, set: function(_){rightAlign=_;}},
+        maxKeyLength:   {get: function(){return maxKeyLength;}, set: function(_){maxKeyLength=_;}},
+        padding:        {get: function(){return padding;}, set: function(_){padding=_;}},
+        updateState:    {get: function(){return updateState;}, set: function(_){updateState=_;}},
+        radioButtonMode:{get: function(){return radioButtonMode;}, set: function(_){radioButtonMode=_;}},
+        expanded:       {get: function(){return expanded;}, set: function(_){expanded=_;}},
+        vers:           {get: function(){return vers;}, set: function(_){vers=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+/* 
+Improvements:
+- consistenly apply no-hover classes to rect isntead of to containing g, see example CSS style for .no-hover rect, rect.no-hover
+- row/column order (user specified) or 'ascending' / 'descending'
+- I haven't tested for transitions between changing datasets
+*/
+
+nv.models.heatMap = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , width = 960
+        , height = 500
+        , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+        , container
+        , xScale = d3.scale.ordinal()
+        , yScale = d3.scale.ordinal()
+        , colorScale = false
+        , getX = function(d) { return d.x }
+        , getY = function(d) { return d.y }
+        , getCellValue = function(d) { return d.value }
+        , showCellValues = true
+        , cellValueFormat = function(d) { return typeof d === 'number' ? d.toFixed(0) : d }
+        , cellAspectRatio = false // width / height of cell
+        , cellRadius = 2
+        , cellBorderWidth = 4 // pixels between cells
+        , normalize = false
+        , highContrastText = true
+        , xDomain
+        , yDomain
+        , xMetaColorScale = nv.utils.defaultColor()
+        , yMetaColorScale = nv.utils.defaultColor()
+        , missingDataColor = '#bcbcbc'
+        , missingDataLabel = ''
+        , metaOffset = 5 // spacing between meta rects and cells
+        , xRange
+        , yRange
+        , xMeta
+        , yMeta
+        , colorRange
+        , colorDomain
+        , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+        , duration = 250
+        , xMetaHeight = function(d) { return cellHeight / 3 }
+        , yMetaWidth = function(d) { return cellWidth / 3 }
+        , showGrid = false
+        ;
+
+
+
+    //============================================================
+    // Aux helper function for heatmap
+    //------------------------------------------------------------
+    // choose high contrast text color based on background
+    // shameful steal: https://github.com/alexandersimoes/d3plus/blob/master/src/color/text.coffee
+    function cellTextColor(bgColor) {
+
+        if (highContrastText) {
+            var rgbColor = d3.rgb(bgColor);
+            var r = rgbColor.r;
+            var g = rgbColor.g;
+            var b = rgbColor.b;
+            var yiq = (r * 299 + g * 587 + b * 114) / 1000;
+            return yiq >= 128 ? "#404040" : "#EDEDED"; // dark text else light text
+        } else {
+            return 'black';
+        }
+    }
+
+    /* go through heatmap data and generate array of values
+     * for each row/column or for entire dataset; for use in
+     * calculating means/medians of data for normalizing
+     * @param {str} axis - 'row', 'col' or null
+     *
+     * @returns {row/column index: [array of values for row/col]}
+     * note that if axis is not specified, the return will be
+     * {0: [all values in heatmap]}
+     */
+    function getHeatmapValues(data, axis) {
+        var vals = {};
+
+        data.forEach(function(cell, i) {
+            if (axis == 'row') {
+                if (!(getIY(cell) in vals)) vals[getIY(cell)] = [];
+                vals[getIY(cell)].push(getCellValue(cell));
+            } else if (axis == 'col') {
+                if (!(getIX(cell) in vals)) vals[getIX(cell)] = [];
+                vals[getIX(cell)].push(getCellValue(cell));
+            } else if (axis == null) { // if calculating stat over entire dataset
+                if (!(0 in vals)) vals[0] = [];
+                vals[0].push(getCellValue(cell)); 
+            }
+        })
+
+        return vals;
+    }
+
+    // calculate the median absolute deviation of the given array of data
+    // https://en.wikipedia.org/wiki/Median_absolute_deviation
+    // MAD = median(abs(Xi - median(X)))
+    function mad(dat) {
+        var med = d3.median(dat);
+        var vals = dat.map(function(d) { return Math.abs(d - med); })
+        return d3.median(vals);
+    }
+
+
+    // set cell color based on cell value
+    // depending on whether it should be normalized or not
+    function cellColor(d) {
+        var colorVal = normalize ? getNorm(d) : getCellValue(d);
+        return (cellsAreNumeric() && !isNaN(colorVal) || typeof colorVal !== 'undefined') ? colorScale(colorVal) : missingDataColor;
+    }
+
+    // return the domain of the color data
+    // if ordinal data is given for the cells, this will
+    // return all possible cells values; otherwise it
+    // returns the extent of the cell values
+    // will take into account normalization if specified
+    function getColorDomain() {
+    
+        if (cellsAreNumeric()) { // if cell values are numeric
+            return normalize ? d3.extent(prepedData, function(d) { return getNorm(d); }) : d3.extent(uniqueColor);
+        } else if (!cellsAreNumeric()) { // if cell values are ordinal
+            return uniqueColor;
+        }
+    }
+
+    // return true if cells are numeric
+    // as opposed to categorical
+    function cellsAreNumeric() {
+        return typeof uniqueColor[0] === 'number';
+    }
+
+    /*
+     * Normalize input data
+     *
+     * normalize must be one of centerX, robustCenterX, centerScaleX, robustCenterScaleX, centerAll, 
+     * robustCenterAll, centerScaleAll, robustCenterScaleAll where X is either 'Row' or 'Column'
+     *
+     * - centerX: subtract row/column mean from cell
+     * - centerAll: subtract mean of whole data set from cell
+     * - centerScaleX: scale so that row/column has mean 0 and variance 1 (Z-score)
+     * - centerScaleAll: scale by overall normalization factor so that the whole data set has mean 0 and variance 1 (Z-score)
+     * - robustCenterX: subtract row/column median from cell
+     * - robustCenterScaleX: subtract row/column median from cell and then scale row/column by median absolute deviation
+     * - robustCenterAll: subtract median of whole data set from cell
+     * - robustCenterScaleAll: subtract overall median from cell and scale by overall median absolute deviation
+     */
+    function normalizeData(dat) {
+        
+        var normTypes = ['centerRow',
+            'robustCenterRow',
+            'centerScaleRow',
+            'robustCenterScaleRow',
+            'centerColumn',
+            'robustCenterColumn',
+            'centerScaleColumn',
+            'robustCenterScaleColumn',
+            'centerAll',
+            'robustCenterAll',
+            'centerScaleAll',
+            'robustCenterScaleAll'];
+
+
+        if(normTypes.indexOf(normalize) != -1) {
+
+            var xVals = Object.keys(uniqueX), yVals = Object.keys(uniqueY);
+
+            // setup normalization options
+            var scale = normalize.includes('Scale') ? true: false,
+                agg = normalize.includes('robust') ? 'median': 'mean',
+                axis = normalize.includes('Row') ? 'row' : normalize.includes('Column') ? 'col' : null,
+                vals = getHeatmapValues(dat, axis);
+
+            // calculate mean or median
+            // calculate standard dev or median absolute deviation
+            var stat = {};
+            var dev = {};
+            for (var key in vals) {
+                stat[key] = agg == 'mean' ? d3.mean(vals[key]) : d3.median(vals[key]);
+                if (scale) dev[key] = agg == 'mean' ? d3.deviation(vals[key]) : mad(vals[key]);
+            }
+
+
+            // do the normalizing
+            dat.forEach(function(cell, i) {
+                if (cellsAreNumeric()) {
+                    if (axis == 'row') {
+                        var key = getIY(cell);
+                    } else if (axis == 'col') {
+                        var key = getIX(cell);
+                    } else if (axis == null) {  // if calculating stat over entire dataset
+                        var key = 0;
+                    }
+
+                    var normVal = getCellValue(cell) - stat[key];
+                    if (scale) {
+                        cell._cellPos.norm = normVal / dev[key];
+                    } else {
+                        cell._cellPos.norm = normVal;
+                    }
+                } else {
+                    cell._cellPos.norm = getCellValue(cell); // if trying to normalize ordinal cells, just set norm to cell value
+                }
+            })
+
+        } else {
+            normalize = false; // proper normalize option was not provided, disable it so heatmap still shows colors
+        }
+
+        return dat;
+    }
+
+    /*
+     * Process incoming data for use with heatmap including:
+     * - adding a unique key indexer to each data point (idx)
+     * - getting a unique list of all x & y values
+     * - generating a position index (x & y) for each data point
+     * - sorting that data for correct traversal when generating rect
+     * - generating placeholders for missing data
+     *
+     * In order to allow for the flexibility of the user providing either
+     * categorical or quantitative data, we're going to position the cells
+     * through indices that we increment based on previously seen data
+     * this way we can use ordinal() axes even if the data is quantitative.
+     *
+     * When we generate the SVG elements, we assumes traversal occures from
+     * top to bottom and from left to right.
+     *
+     * @param data {list} - input data organize as a list of objects
+     *
+     * @return - copy of input data with additional '_cellPos' key
+     *           formatted as {idx: XXX, ix, XXX, iy: XXX}
+     *           where idx is a global identifier; ix is an identifier
+     *           within each column, and iy is an identifier within
+     *           each row. 
+     */
+    function prepData(data) {
+
+        // reinitialize
+        uniqueX = {}, // {cell x value: ix index}
+        uniqueY = {}, // {cell y value: iy index}
+        uniqueColor = [], // [cell color value]
+        uniqueXMeta = [], // [cell x metadata value]
+        uniqueYMeta = [], // [cell y metadata value]
+        uniqueCells = []; // [cell x,y values stored as array]
+        var warnings = [];
+        var sortedCells = {}; // {cell x values: {cell y value: cell data, ... }, ... }
+
+        var ix = 0, iy = 0; // use these indices to position cell in x & y direction
+        var combo, idx=0;
+        data.forEach(function(cell) {
+            var valX = getX(cell),
+                valY = getY(cell),
+                valColor = getCellValue(cell);            
+
+            // assemble list of unique values for each dimension
+            if (!(valX in uniqueX)) { 
+                uniqueX[valX] = ix; 
+                ix++;
+
+                sortedCells[valX] = {}
+
+                if (typeof xMeta === 'function') uniqueXMeta.push(xMeta(cell));
+            }
+
+            if (!(valY in uniqueY)) {
+                uniqueY[valY] = iy; 
+                iy++;
+
+                sortedCells[valX][valY] = {}
+
+                if (typeof yMeta === 'function') uniqueYMeta.push(yMeta(cell));
+            }
+            if (uniqueColor.indexOf(valColor) == -1) uniqueColor.push(valColor)
+
+
+            // for each data point, we generate an object of data
+            // needed to properly position each cell
+            cell._cellPos = {
+                idx: idx,
+                ix: uniqueX[valX],
+                iy: uniqueY[valY],
+            }
+            idx++;
+
+
+            // keep track of row & column combinations we've already seen
+            // this prevents the same cells from being generated when
+            // the user hasn't provided proper data (one value for each
+            // row & column).
+            // if properly formatted data is not provided, only the first
+            // row & column value is used (the rest are ignored)
+            combo = [valX, valY];
+            if (!isArrayInArray(uniqueCells, combo)) {
+                uniqueCells.push(combo)
+                sortedCells[valX][valY] = cell;
+            } else if (warnings.indexOf(valX + valY) == -1) {
+                warnings.push(valX + valY);
+                console.warn("The row/column position " + valX + "/" + valY + " has multiple values; ensure each cell has only a single value.");
+            }
+
+        });
+
+        uniqueColor = uniqueColor.sort()
+
+        // check in sortedCells that each x has all the y's
+        // if not, generate an empty placeholder
+        // this will also sort all cells from left to right
+        // and top to bottom
+        var reformatData = [];
+        Object.keys(uniqueY).forEach(function(j) {
+            Object.keys(uniqueX).forEach(function(i) {
+                var cellVal = sortedCells[i][j];
+    
+                if (cellVal) {
+                    reformatData.push(cellVal);
+                } else {
+                    var cellPos = {
+                        idx: idx,
+                        ix: uniqueX[i],
+                        iy: uniqueY[j],
+                    }
+                    idx++;
+                    reformatData.push({_cellPos: cellPos}); // empty cell placeholder
+                }
+            })
+        })
+
+
+        // normalize data is needed
+        return normalize ? normalizeData(reformatData) : reformatData;
+
+    }
+
+    // https://stackoverflow.com/a/41661388/1153897
+    function isArrayInArray(arr, item){
+      var item_as_string = JSON.stringify(item);
+
+      var contains = arr.some(function(ele){
+        return JSON.stringify(ele) === item_as_string;
+      });
+      return contains;
+    }
+
+    function removeAllHoverClasses() {
+        // remove all hover classes
+        d3.selectAll('.cell-hover').classed('cell-hover', false);
+        d3.selectAll('.no-hover').classed('no-hover', false);
+        d3.selectAll('.row-hover').classed('row-hover', false);
+        d3.selectAll('.column-hover').classed('column-hover', false);
+    }
+
+    // return the formatted cell value if it is
+    // a number, otherwise return missingDataLabel
+    var cellValueLabel = function(d) {
+        var val = !normalize ? cellValueFormat(getCellValue(d)) : cellValueFormat(getNorm(d));
+        return (cellsAreNumeric() && !isNaN(val) || typeof val !== 'undefined') ? val : missingDataLabel;
+    }
+
+    // https://stackoverflow.com/a/16794116/1153897
+    // note this returns the obj keys
+    function sortObjByVals(obj) {
+        return Object.keys(obj).sort(function(a,b){return obj[a]-obj[b]})
+    }
+
+    // https://stackoverflow.com/a/28191966/1153897
+    function getKeyByValue(object, value) {
+        //return Object.keys(object).find(key => object[key] === value);
+        return Object.keys(object).filter(function(key) {return object[key] === value})[0];
+    }
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var prepedData, cellHeight, cellWidth;
+    var uniqueX = {}, uniqueY = {}, uniqueColor = [];
+    var uniqueXMeta = [], uniqueYMeta = [], uniqueCells = []
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+    var RdYlBu = ["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"];
+
+    var getCellPos = function(d) { return d._cellPos; };
+    var getIX = function(d) { return getCellPos(d).ix; } // get the given cell's x index position
+    var getIY = function(d) { return getCellPos(d).iy; } // get the given cell's y index position
+    var getNorm = function(d) { return getCellPos(d).norm; }
+    var getIdx = function(d) { return getCellPos(d).idx; }
+
+
+    function chart(selection) {
+        renderWatch.reset();
+        selection.each(function(data) {
+
+            prepedData = prepData(data);
+
+            var availableWidth = width - margin.left - margin.right,
+                availableHeight = height - margin.top - margin.bottom;
+
+            // available width/height set the cell dimenions unless
+            // the aspect ratio is defined - in that case the cell
+            // height is adjusted and availableHeight updated
+            cellWidth = availableWidth / Object.keys(uniqueX).length;
+            cellHeight = cellAspectRatio ? cellWidth / cellAspectRatio : availableHeight / Object.keys(uniqueY).length;
+            if (cellAspectRatio) availableHeight = cellHeight * Object.keys(uniqueY).length - margin.top - margin.bottom;
+
+
+            container = d3.select(this);
+            nv.utils.initSVG(container);
+  
+            // Setup Scales
+            xScale.domain(xDomain || sortObjByVals(uniqueX))
+                  .rangeBands(xRange || [0, availableWidth-cellBorderWidth/2]);
+            yScale.domain(yDomain || sortObjByVals(uniqueY))
+                  .rangeBands(yRange || [0, availableHeight-cellBorderWidth/2]);
+            colorScale = cellsAreNumeric() ? d3.scale.quantize() : d3.scale.ordinal();
+            colorScale.domain(colorDomain || getColorDomain())
+                  .range(colorRange || RdYlBu);
+
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-heatMapWrap').data([prepedData]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-heatMapWrap');
+            wrapEnter
+                .append('g')
+                .attr('class','cellWrap')
+
+            wrap.watchTransition(renderWatch, 'nv-wrap: heatMapWrap')
+                .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            var gridWrap = wrapEnter
+                .append('g')
+                .attr('class','cellGrid')
+                .style('opacity',1e-6)
+
+            var gridLinesV = wrap.select('.cellGrid').selectAll('.gridLines.verticalGrid')
+                .data(Object.values(uniqueX).concat([Object.values(uniqueX).length]))
+                
+            gridLinesV.enter()
+                .append('line')
+                .attr('class','gridLines verticalGrid')
+
+            gridLinesV.exit()
+                .remove()
+
+            var gridLinesH = wrap.select('.cellGrid').selectAll('.gridLines.horizontalGrid')
+                .data(Object.values(uniqueY).concat([Object.values(uniqueY).length]))
+                
+            gridLinesH.enter()
+                .append('line')
+                .attr('class','gridLines horizontalGrid')
+
+            gridLinesH.exit()
+                .remove()
+
+            var cellWrap = wrap.select('.cellWrap')
+                .selectAll(".nv-cell")
+                .data(function(d) { return d; }, function(e) { return getIdx(e); })
+
+            var xMetaWrap = wrapEnter
+                .append('g')
+                .attr('class','xMetaWrap')
+                .attr("transform", function() { return "translate(0," + (-xMetaHeight()-cellBorderWidth-metaOffset) + ")" })
+
+            var xMetas = wrap.select('.xMetaWrap').selectAll('.x-meta')
+                .data(uniqueXMeta)
+
+            var xMetaEnter = xMetas
+                .enter()
+                .append('rect')
+                .attr('class','x-meta meta')
+                .attr("width", cellWidth-cellBorderWidth)
+                .attr("height", xMetaHeight())
+                .attr("transform", "translate(0,0)")
+                .attr("fill", function(d) { return xMetaColorScale(d); })
+
+            var yMetaWrap = wrapEnter
+                .append('g')
+                .attr('class','yMetaWrap')
+                .attr("transform", function(d,i) { return "translate(" + (-yMetaWidth()-cellBorderWidth-metaOffset) + ",0)" })
+
+            var yMetas = wrap.select('.yMetaWrap').selectAll('.y-meta')
+                .data(uniqueYMeta)
+
+            var yMetaEnter = yMetas
+                .enter()
+                .append('rect')
+                .attr('class','y-meta meta')
+                .attr("width", yMetaWidth())
+                .attr("height", cellHeight-cellBorderWidth)
+                .attr("transform", function(d,i) { return "translate(0,0)" })
+                .attr("fill", function(d,i) { return yMetaColorScale(d); })
+
+            xMetas.exit().remove()
+            yMetas.exit().remove()
+          
+            // CELLS    
+            var cellsEnter = cellWrap
+                .enter()
+                .append('g')
+                .style('opacity', 1e-6)
+                .attr("transform", function(d) { return "translate(0," + getIY(d) * cellHeight + ")" }) // enter all g's here for a sweep-right transition
+                .attr('data-row', function(d) { return getIY(d) })
+                .attr('data-column', function(d) { return getIX(d) });
+
+            cellsEnter
+                .append("rect") 
+
+            cellsEnter
+                .append('text')
+                .attr('text-anchor', 'middle')
+                .attr("dy", 4)
+                .attr("class","cell-text")
+
+            
+            // transition cell (rect) size
+            cellWrap.selectAll('rect')
+                .watchTransition(renderWatch, 'heatMap: rect')
+                .attr("width", cellWidth-cellBorderWidth)
+                .attr("height", cellHeight-cellBorderWidth)
+                .attr('rx', cellRadius)
+                .attr('ry', cellRadius)
+                .style('stroke', function(d) { return cellColor(d) })
+
+            // transition cell (g) position, opacity and fill
+            cellWrap
+                .attr("class",function(d) { return isNaN(getCellValue(d)) ? 'nv-cell cell-missing' : 'nv-cell'}) 
+                .watchTransition(renderWatch, 'heatMap: cells')
+                .style({
+                    'opacity': 1,
+                    'fill': function(d) { return cellColor(d) },
+                })
+                .attr("transform", function(d) { return "translate(" + getIX(d) * cellWidth + "," + getIY(d) * cellHeight + ")" })
+                .attr("class",function(d) { return isNaN(getCellValue(d)) ? 'nv-cell cell-missing' : 'nv-cell'}) 
+
+            cellWrap.exit().remove();
+
+            // transition text position and fill
+            cellWrap.selectAll('text')
+                .watchTransition(renderWatch, 'heatMap: cells text')
+                .text(function(d) { return cellValueLabel(d); })
+                .attr("x", function(d) { return (cellWidth-cellBorderWidth) / 2; })
+                .attr("y", function(d) { return (cellHeight-cellBorderWidth) / 2; })
+                .style("fill", function(d) { return cellTextColor(cellColor(d)) })
+                .style('opacity', function() { return showCellValues ? 1 : 0 })
+
+            // transition grid
+            wrap.selectAll('.verticalGrid')
+                .watchTransition(renderWatch, 'heatMap: gridLines') 
+                .attr('y1',0)
+                .attr('y2',availableHeight-cellBorderWidth)
+                .attr('x1',function(d) { return d*cellWidth-cellBorderWidth/2; })
+                .attr('x2',function(d) { return d*cellWidth-cellBorderWidth/2; })
+
+            var numHLines = Object.keys(uniqueY).length;
+            wrap.selectAll('.horizontalGrid')
+                .watchTransition(renderWatch, 'heatMap: gridLines') 
+                .attr('x1',function(d) { return (d == 0 || d == numHLines) ? -cellBorderWidth : 0 })
+                .attr('x2',function(d) { return (d == 0 || d == numHLines) ? availableWidth : availableWidth-cellBorderWidth})
+                .attr('y1',function(d) { return d*cellHeight-cellBorderWidth/2; })
+                .attr('y2',function(d) { return d*cellHeight-cellBorderWidth/2; })
+
+            wrap.select('.cellGrid')
+                .watchTransition(renderWatch, 'heatMap: gridLines')
+                .style({
+                    'stroke-width': cellBorderWidth,
+                    'opacity': function() { return showGrid ? 1 : 1e-6 },
+                })
+
+            var xMetaRect = wrap.selectAll('.x-meta')
+            var yMetaRect = wrap.selectAll('.y-meta')
+            var allMetaRect = wrap.selectAll('.meta')
+
+            // transition meta rect size
+            xMetas
+                .watchTransition(renderWatch, 'heatMap: xMetaRect') 
+                .attr("width", cellWidth-cellBorderWidth)
+                .attr("height", xMetaHeight())
+                .attr("transform", function(d,i) { return "translate(" + (i * cellWidth) + ",0)" })
+
+            yMetas
+                .watchTransition(renderWatch, 'heatMap: yMetaRect') 
+                .attr("width", yMetaWidth())
+                .attr("height", cellHeight-cellBorderWidth)
+                .attr("transform", function(d,i) { return "translate(0," + (i * cellHeight) + ")" })
+
+
+            // transition position of meta wrap g & opacity
+            wrap.select('.xMetaWrap')
+                .watchTransition(renderWatch, 'heatMap: xMetaWrap') 
+                .attr("transform", function(d,i) { return "translate(0," + (-xMetaHeight()-cellBorderWidth-metaOffset) + ")" })
+                .style("opacity", function() { return xMeta !== false ? 1 : 0 })
+            wrap.select('.yMetaWrap')
+                .watchTransition(renderWatch, 'heatMap: yMetaWrap') 
+                .attr("transform", function(d,i) { return "translate(" + (-yMetaWidth()-cellBorderWidth-metaOffset) + ",0)" })
+                .style("opacity", function() { return yMeta !== false ? 1 : 0 })
+
+            // TOOLTIPS
+            cellWrap
+                .on('mouseover', function(d,i) {
+
+                    var idx = getIdx(d);
+                    var ix = getIX(d);
+                    var iy = getIY(d);
+
+                    // set the proper classes for all cells
+                    // hover row gets class .row-hover
+                    // hover column gets class .column-hover
+                    // hover cell gets class .cell-hover
+                    // all remaining cells get class .no-hover
+                    d3.selectAll('.nv-cell').each(function(e) {
+                        if (idx == getIdx(e)) {
+                            d3.select(this).classed('cell-hover', true);
+                            d3.select(this).classed('no-hover', false);
+                        } else {
+                            d3.select(this).classed('no-hover', true);
+                            d3.select(this).classed('cell-hover', false);
+                        }
+                        if (ix == getIX(e)) {
+                            d3.select(this).classed('no-hover', false);
+                            d3.select(this).classed('column-hover', true);
+                        }
+                        if (iy == getIY(e)) {
+                            d3.select(this).classed('no-hover', false);
+                            d3.select(this).classed('row-hover', true);
+                        }
+                    })
+    
+                    // set hover classes for column metadata
+                    d3.selectAll('.x-meta').each(function(e, j) {
+                        if (j == ix) {
+                            d3.select(this).classed('cell-hover', true);
+                            d3.select(this).classed('no-hover', false);
+                        } else {
+                            d3.select(this).classed('no-hover', true);
+                            d3.select(this).classed('cell-hover', false);
+                        }
+                    });
+
+                    // set hover class for row metadata
+                    d3.selectAll('.y-meta').each(function(e, j) {
+                        if (j == iy) {
+                            d3.select(this).classed('cell-hover', true);
+                            d3.select(this).classed('no-hover', false);
+                        } else {
+                            d3.select(this).classed('no-hover', true);
+                            d3.select(this).classed('cell-hover', false);
+                        }
+                    });
+                    
+                    dispatch.elementMouseover({
+                        value: getKeyByValue(uniqueX, ix) + ' & ' + getKeyByValue(uniqueY, iy), 
+                        series: {
+                                value: cellValueLabel(d), 
+                                color: d3.select(this).select('rect').style("fill")
+                                },
+                        e: d3.event,
+                    });
+
+                })
+                .on('mouseout', function(d,i) {
+
+                    // allow tooltip to remain even when mouse is over the
+                    // space between the cell;
+                    // this prevents cells from "flashing" when transitioning
+                    // between cells
+                    var bBox = d3.select(this).select('rect').node().getBBox();
+                    var coordinates = d3.mouse(d3.select('.nv-heatMap').node());
+                    var x = coordinates[0];
+                    var y = coordinates[1];
+
+                    // we only trigger mouseout when mouse moves outside of
+                    // .nv-heatMap
+                    if (x + cellBorderWidth >= availableWidth || y + cellBorderWidth >= availableHeight || x < 0 || y < 0) {
+                        // remove all hover classes
+                        removeAllHoverClasses();
+
+                        dispatch.elementMouseout({e: d3.event});
+                    }
+                })
+                .on('mousemove', function(d,i) {
+
+                    dispatch.elementMousemove({e: d3.event});
+                })
+
+            allMetaRect
+                .on('mouseover', function(d,i) {
+
+                    // true if hovering over a row metadata rect
+                    var isColMeta = d3.select(this).attr('class').indexOf('x-meta') != -1 ? true : false;
+
+                    // apply proper .row-hover & .column-hover
+                    // classes to cells
+                    d3.selectAll('.nv-cell').each(function(e) {
+
+                        if (isColMeta && i == getIX(e)) {
+                            d3.select(this).classed('column-hover', true);
+                            d3.select(this).classed('no-hover', false);
+                        } else if (!isColMeta && i-uniqueXMeta.length == getIY(e)) {
+                            // since allMetaRect selects all the meta rects, the index for the y's will
+                            // be offset by the number of x rects. TODO - write seperate tooltip sections
+                            // for x meta rect & y meta rect
+                            d3.select(this).classed('row-hover', true);
+                            d3.select(this).classed('no-hover', false);
+                        } else {
+                            d3.select(this).classed('no-hover', true);
+                            d3.select(this).classed('column-hover', false);
+                            d3.select(this).classed('row-hover', false);
+                        }
+                        d3.select(this).classed('cell-hover', false);
+                    })
+
+                    // apply proper .row-hover & .column-hover
+                    // classes to meta rects
+                    d3.selectAll('.meta').classed('no-hover', true);
+                    d3.select(this).classed('cell-hover', true);
+                    d3.select(this).classed('no-hover', false);
+
+                    dispatch.elementMouseover({
+                        value: isColMeta ? 'Column meta' : 'Row meta',
+                        series: { value: d, color: d3.select(this).style('fill'), }
+                    });
+                })
+                .on('mouseout', function(d,i) {
+
+                    // true if hovering over a row metadata rect
+                    var isColMeta = d3.select(this).attr('class').indexOf('x-meta') != -1 ? true : false;
+
+                    // allow tooltip to remain even when mouse is over the
+                    // space between the cell;
+                    // this prevents cells from "flashing" when transitioning
+                    // between cells
+                    var bBox = d3.select(this).node().getBBox();
+                    var coordinates = d3.mouse(d3.select(isColMeta ? '.xMetaWrap' : '.yMetaWrap').node());
+                    var x = coordinates[0];
+                    var y = coordinates[1];
+
+                    if ( y < 0 || x < 0 || 
+                        (isColMeta && x + cellBorderWidth >= availableWidth) ||
+                        (!isColMeta && y + cellBorderWidth >= availableHeight)
+                    ) {
+                        // remove all hover classes
+                        removeAllHoverClasses();
+
+                        dispatch.elementMouseout({e: d3.event});
+                    }
+                })
+                .on('mousemove', function(d,i) {
+                    dispatch.elementMousemove({e: d3.event});
+                })
+
+        });
+
+
+        renderWatch.renderEnd('heatMap immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:   {get: function(){return width;}, set: function(_){width=_;}},
+        height:  {get: function(){return height;}, set: function(_){height=_;}},
+        showCellValues: {get: function(){return showCellValues;}, set: function(_){showCellValues=_;}},
+        x:       {get: function(){return getX;}, set: function(_){getX=_;}}, // data attribute for horizontal axis
+        y:       {get: function(){return getY;}, set: function(_){getY=_;}}, // data attribute for vertical axis
+        cellValue:       {get: function(){return getCellValue;}, set: function(_){getCellValue=_;}}, // data attribute that sets cell value and color
+        missingDataColor:  {get: function(){return missingDataColor;}, set: function(_){missingDataColor=_;}},
+        missingDataLabel:  {get: function(){return missingDataLabel;}, set: function(_){missingDataLabel=_;}},
+        xScale:  {get: function(){return xScale;}, set: function(_){xScale=_;}},
+        yScale:  {get: function(){return yScale;}, set: function(_){yScale=_;}},
+        colorScale:  {get: function(){return colorScale;}, set: function(_){colorScale=_;}}, // scale to map cell values to colors
+        xDomain:  {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+        yDomain:  {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+        xRange:  {get: function(){return xRange;}, set: function(_){xRange=_;}},
+        yRange:  {get: function(){return yRange;}, set: function(_){yRange=_;}},
+        colorRange:  {get: function(){return colorRange;}, set: function(_){colorRange=_;}},
+        colorDomain:  {get: function(){return colorDomain;}, set: function(_){colorDomain=_;}},
+        xMeta:  {get: function(){return xMeta;}, set: function(_){xMeta=_;}},
+        yMeta:  {get: function(){return yMeta;}, set: function(_){yMeta=_;}},
+        xMetaColorScale:  {get: function(){return color;}, set: function(_){color = nv.utils.getColor(_);}},
+        yMetaColorScale:  {get: function(){return color;}, set: function(_){color = nv.utils.getColor(_);}},
+        cellAspectRatio:  {get: function(){return cellAspectRatio;}, set: function(_){cellAspectRatio=_;}}, // cell width / height
+        cellRadius:  {get: function(){return cellRadius;}, set: function(_){cellRadius=_;}}, // cell width / height
+        cellHeight:  {get: function(){return cellHeight;}}, // TODO - should not be exposed since we don't want user setting this
+        cellWidth:   {get: function(){return cellWidth;}}, // TODO - should not be exposed since we don't want user setting this
+        normalize:   {get: function(){return normalize;}, set: function(_){normalize=_;}},
+        cellBorderWidth:     {get: function(){return cellBorderWidth;}, set: function(_){cellBorderWidth=_;}},
+        highContrastText:    {get: function(){return highContrastText;}, set: function(_){highContrastText=_;}},
+        cellValueFormat:     {get: function(){return cellValueFormat;}, set: function(_){cellValueFormat=_;}},
+        id:                  {get: function(){return id;}, set: function(_){id=_;}},
+        metaOffset:          {get: function(){return metaOffset;}, set: function(_){metaOffset=_;}},
+        xMetaHeight:         {get: function(){return xMetaHeight;}, set: function(_){xMetaHeight=_;}},
+        yMetaWidth:          {get: function(){return yMetaWidth;}, set: function(_){yMetaWidth=_;}},
+        showGrid:          {get: function(){return showGrid;}, set: function(_){showGrid=_;}},
+
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+
+
+    return chart;
+};
+/* Heatmap Chart Type
+
+A heatmap is a graphical representation of data where the individual values
+contained in a matrix are represented as colors within cells. Furthermore,
+metadata can be associated with each of the matrix rows or columns. By grouping
+these rows/columns together by a given metadata value, data trends can be spotted.
+
+Format for input data should be:
+var data = [
+    {day: 'mo', hour: '1a', value: 16, timeperiod: 'early morning', weekperiod: 'week', category: 1},
+    {day: 'mo', hour: '2a', value: 20, timeperiod: 'early morning', weekperiod: 'week', category: 2},
+    {day: 'mo', hour: '3a', value: 0, timeperiod: 'early morning', weekperiod: 'week', category: 1},
+    ...
+]
+where the keys 'day' and 'hour' specify the row/column of the heatmap, 'value' specifies the  cell
+value and the keys 'timeperiod', 'weekperiod' and 'week' are extra metadata that can be associated
+with rows/columns.
+
+
+Options for chart:
+*/
+nv.models.heatMapChart = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var heatMap = nv.models.heatMap()
+        , legend = nv.models.legend()
+        , legendRowMeta = nv.models.legend()
+        , legendColumnMeta = nv.models.legend()
+        , tooltip = nv.models.tooltip()
+        , xAxis = nv.models.axis()
+        , yAxis = nv.models.axis()
+        ;
+
+
+    var margin = {top: 20, right: 10, bottom: 50, left: 60}
+        , marginTop = null
+        , width = null
+        , height = null
+        , color = nv.utils.getColor()
+        , showLegend = true
+        , staggerLabels = false
+        , showXAxis = true
+        , showYAxis = true
+        , alignYAxis = 'left'
+        , alignXAxis = 'top'
+        , rotateLabels = 0
+        , title = false
+        , x
+        , y
+        , noData = null
+        , dispatch = d3.dispatch('beforeUpdate','renderEnd')
+        , duration = 250
+        ;
+
+    xAxis
+        .orient(alignXAxis)
+        .showMaxMin(false)
+        .tickFormat(function(d) { return d })
+    ;
+    yAxis
+        .orient(alignYAxis)
+        .showMaxMin(false)
+        .tickFormat(function(d) { return d })
+    ;
+
+    tooltip
+        .duration(0)
+        .headerEnabled(true)
+        .keyFormatter(function(d, i) {
+            return xAxis.tickFormat()(d, i);
+        })
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    // https://bl.ocks.org/mbostock/4573883
+    // get max/min range for all the quantized cell values
+    // returns an array where each element is [start,stop]
+    // of color bin
+    function quantizeLegendValues() {
+
+        var e = heatMap.colorScale(), legendVals;
+
+        if (typeof e.domain()[0] === 'string') { // if color scale is ordinal
+
+            legendVals = e.domain();
+
+        } else { // if color scale is numeric
+
+            legendVals = e.range().map(function(color) {
+              var d = e.invertExtent(color);
+              if (d[0] === null) d[0] = e.domain()[0];
+              if (d[1] === null) d[1] = e.domain()[1];
+              return d;
+            })
+
+        }
+
+        return legendVals
+
+    }
+
+    // return true if row metadata specified by user
+    function hasRowMeta() {
+        return typeof heatMap.yMeta() === 'function'
+    }
+    // return true if col metadata specified by user
+    function hasColumnMeta() {
+        return typeof heatMap.xMeta() === 'function'
+    }
+
+    var renderWatch = nv.utils.renderWatch(dispatch, duration);
+
+    function chart(selection) {
+        renderWatch.reset();
+        renderWatch.models(heatMap);
+        renderWatch.models(xAxis);
+        renderWatch.models(yAxis);
+
+        selection.each(function(data) {
+            var container = d3.select(this),
+                that = this;
+            nv.utils.initSVG(container);
+
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = nv.utils.availableHeight(height, container, margin);
+
+            chart.update = function() {
+                dispatch.beforeUpdate();
+                container.transition().duration(duration).call(chart);
+            };
+            chart.container = this;
+
+            // Display No Data message if there's nothing to show.
+            if (!data || !data.length) {
+                nv.utils.noData(chart, container);
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            // Setup Scales
+            x = heatMap.xScale();
+            y = heatMap.yScale();
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap').append('g');
+            var g = wrap.select('g');
+
+
+            gEnter.append('g').attr('class', 'nv-heatMap');
+            gEnter.append('g').attr('class', 'nv-legendWrap');
+            gEnter.append('g').attr('class', 'nv-x nv-axis');
+            gEnter.append('g').attr('class', 'nv-y nv-axis')
+
+            g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+
+            heatMap
+                .width(availableWidth)
+                .height(availableHeight);
+
+
+            var heatMapWrap = g.select('.nv-heatMap')
+                .datum(data.filter(function(d) { return !d.disabled }));
+
+
+            heatMapWrap.transition().call(heatMap);
+
+
+            if (heatMap.cellAspectRatio()) {
+                availableHeight = heatMap.cellHeight() * y.domain().length;
+                heatMap.height(availableHeight);
+            }
+
+
+            // Setup Axes
+            xAxis
+                .scale(x)
+                ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+                .tickSize(-availableHeight, 0);
+
+            var axisX = g.select('.nv-x.nv-axis')
+
+            axisX.call(xAxis)
+                .watchTransition(renderWatch, 'heatMap: axisX')
+                .selectAll('.tick')
+                .style('opacity', function() { return showXAxis ? 1 : 0 } )
+
+            var xTicks = axisX.selectAll('g');
+
+            xTicks
+                .selectAll('.tick text')
+                .attr('transform', function(d,i,j) {
+                    var rot = rotateLabels != 0 ? rotateLabels : '0';
+                    var stagger = staggerLabels ? j % 2 == 0 ? '5' : '17' : '0';
+                    return 'translate(0, ' + stagger + ') rotate(' + rot + ' 0,0)';
+                })
+                .style('text-anchor', rotateLabels > 0 ? 'start' : rotateLabels < 0 ? 'end' : 'middle');
+
+            // position text in center of meta rects
+            var yPos = -5;
+            if (hasColumnMeta()) {
+                axisX.selectAll('text').style('text-anchor', 'middle')
+                yPos = -heatMap.xMetaHeight()()/2 - heatMap.metaOffset() + 3;
+            }
+
+            // adjust position of axis based on presence of metadata group
+            if (alignXAxis == 'bottom') {
+                axisX
+                    .watchTransition(renderWatch, 'heatMap: axisX')
+                    .attr("transform", "translate(0," + (availableHeight - yPos) + ")");
+                if (heatMap.xMeta() !== false) { // if showing x metadata
+                    var pos = availableHeight+heatMap.metaOffset()+heatMap.cellBorderWidth()
+                    g.select('.xMetaWrap')
+                        .watchTransition(renderWatch, 'heatMap: xMetaWrap')
+                        .attr("transform", function(d,i) { return "translate(0," + pos + ")" })
+                }
+            } else {
+                axisX
+                    .watchTransition(renderWatch, 'heatMap: axisX')
+                    .attr("transform", "translate(0," + yPos + ")");
+            }
+
+
+            yAxis
+                .scale(y)
+                ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+                .tickSize( -availableWidth, 0);
+
+            var axisY = g.select('.nv-y.nv-axis')
+
+            axisY.call(yAxis)
+                .watchTransition(renderWatch, 'heatMap: axisY')
+                .selectAll('.tick')
+                .style('opacity', function() { return showYAxis ? 1 : 0 } )
+
+            // position text in center of meta rects
+            var xPos = -5;
+            if (hasRowMeta()) {
+                axisY.selectAll('text').style('text-anchor', 'middle')
+                xPos = -heatMap.yMetaWidth()()/2 - heatMap.metaOffset();
+            }
+
+            // adjust position of axis based on presence of metadata group
+            if (alignYAxis == 'right') {
+                axisY.attr("transform", "translate(" + (availableWidth - xPos) + ",0)");
+                if (heatMap.yMeta() !== false) { // if showing y meatdata
+                    var pos = availableWidth+heatMap.metaOffset()+heatMap.cellBorderWidth()
+                    g.select('.yMetaWrap')
+                        .watchTransition(renderWatch, 'heatMap: yMetaWrap')
+                        .attr("transform", function(d,i) { return "translate(" + pos + ",0)" })
+                }
+            } else {
+                axisY.attr("transform", "translate(" + xPos + ",0)");
+            }
+
+
+
+            // Legend
+            var legendWrap = g.select('.nv-legendWrap')
+
+            legend
+                .width(availableWidth)
+                .color(heatMap.colorScale().range())
+
+            var legendVal = quantizeLegendValues().map(function(d) {
+                if (Array.isArray(d)) { // if cell values are numeric
+                    return {key: d[0].toFixed(1) + " - " + d[1].toFixed(1)};
+                } else { // if cell values are ordinal
+                    return {key: d};
+                }
+            })
+            
+
+            legendWrap
+                .datum(legendVal)
+                .call(legend)
+                .attr('transform', 'translate(0,' + (alignXAxis == 'top' ? availableHeight : -30) + ')'); // TODO: more intelligent offset (-30) when top aligning legend
+
+            legendWrap
+                .watchTransition(renderWatch, 'heatMap: nv-legendWrap')
+                .style('opacity', function() { return showLegend ? 1 : 0 } )
+
+        });
+
+        // axis don't have a flag for disabling the zero line, so we do it manually
+        d3.selectAll('.nv-axis').selectAll('line')
+            .style('stroke-opacity', 0)
+        d3.select('.nv-y').select('path.domain').remove()
+
+        renderWatch.renderEnd('heatMap chart immediate');
+
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    heatMap.dispatch.on('elementMouseover.tooltip', function(evt) {
+        tooltip.data(evt).hidden(false);
+    });
+
+    heatMap.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.hidden(true);
+    });
+
+    heatMap.dispatch.on('elementMousemove.tooltip', function(evt) {
+        tooltip();
+    });
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.heatMap = heatMap;
+    chart.legend = legend;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+        showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+        noData:     {get: function(){return noData;}, set: function(_){noData=_;}},
+        showXAxis:     {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis:     {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}},
+        rotateLabels:  {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            if (_.top !== undefined) {
+                margin.top = _.top;
+                marginTop = _.top;
+            }
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        duration: {get: function(){return duration;}, set: function(_){
+            duration = _;
+            renderWatch.reset(duration);
+            heatMap.duration(duration);
+            xAxis.duration(duration);
+            yAxis.duration(duration);
+        }},
+        alignYAxis: {get: function(){return alignYAxis;}, set: function(_){
+            alignYAxis = _;
+            yAxis.orient(_);
+        }},
+        alignXAxis: {get: function(){return alignXAxis;}, set: function(_){
+            alignXAxis = _;
+            xAxis.orient(_);
+        }},
+    });
+
+    nv.utils.inheritOptions(chart, heatMap);
+    nv.utils.initOptions(chart);
+
+    return chart;
+}
+//TODO: consider deprecating and using multibar with single series for this
+nv.models.historicalBar = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 0, right: 0, bottom: 0, left: 0}
+        , width = null
+        , height = null
+        , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+        , container = null
+        , x = d3.scale.linear()
+        , y = d3.scale.linear()
+        , getX = function(d) { return d.x }
+        , getY = function(d) { return d.y }
+        , forceX = []
+        , forceY = [0]
+        , padData = false
+        , clipEdge = true
+        , color = nv.utils.defaultColor()
+        , xDomain
+        , yDomain
+        , xRange
+        , yRange
+        , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
+        , interactive = true
+        ;
+
+    var renderWatch = nv.utils.renderWatch(dispatch, 0);
+
+    function chart(selection) {
+        selection.each(function(data) {
+            renderWatch.reset();
+
+            container = d3.select(this);
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = nv.utils.availableHeight(height, container, margin);
+
+            nv.utils.initSVG(container);
+
+            // Setup Scales
+            x.domain(xDomain || d3.extent(data[0].values.map(getX).concat(forceX) ));
+
+            if (padData)
+                x.range(xRange || [availableWidth * .5 / data[0].values.length, availableWidth * (data[0].values.length - .5)  / data[0].values.length ]);
+            else
+                x.range(xRange || [0, availableWidth]);
+
+            y.domain(yDomain || d3.extent(data[0].values.map(getY).concat(forceY) ))
+                .range(yRange || [availableHeight, 0]);
+
+            // If scale's domain don't have a range, slightly adjust to make one... so a chart can show a single data point
+            if (x.domain()[0] === x.domain()[1])
+                x.domain()[0] ?
+                    x.domain([x.domain()[0] - x.domain()[0] * 0.01, x.domain()[1] + x.domain()[1] * 0.01])
+                    : x.domain([-1,1]);
+
+            if (y.domain()[0] === y.domain()[1])
+                y.domain()[0] ?
+                    y.domain([y.domain()[0] + y.domain()[0] * 0.01, y.domain()[1] - y.domain()[1] * 0.01])
+                    : y.domain([-1,1]);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-historicalBar-' + id).data([data[0].values]);
+            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-historicalBar-' + id);
+            var defsEnter = wrapEnter.append('defs');
+            var gEnter = wrapEnter.append('g');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-bars');
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            container
+                .on('click', function(d,i) {
+                    dispatch.chartClick({
+                        data: d,
+                        index: i,
+                        pos: d3.event,
+                        id: id
+                    });
+                });
+
+            defsEnter.append('clipPath')
+                .attr('id', 'nv-chart-clip-path-' + id)
+                .append('rect');
+
+            wrap.select('#nv-chart-clip-path-' + id + ' rect')
+                .attr('width', availableWidth)
+                .attr('height', availableHeight);
+
+            g.attr('clip-path', clipEdge ? 'url(#nv-chart-clip-path-' + id + ')' : '');
+
+            var bars = wrap.select('.nv-bars').selectAll('.nv-bar')
+                .data(function(d) { return d }, function(d,i) {return getX(d,i)});
+            bars.exit().remove();
+
+            bars.enter().append('rect')
+                .attr('x', 0 )
+                .attr('y', function(d,i) {  return nv.utils.NaNtoZero(y(Math.max(0, getY(d,i)))) })
+                .attr('height', function(d,i) { return nv.utils.NaNtoZero(Math.abs(y(getY(d,i)) - y(0))) })
+                .attr('transform', function(d,i) { return 'translate(' + (x(getX(d,i)) - availableWidth / data[0].values.length * .45) + ',0)'; })
+                .on('mouseover', function(d,i) {
+                    if (!interactive) return;
+                    d3.select(this).classed('hover', true);
+                    dispatch.elementMouseover({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+
+                })
+                .on('mouseout', function(d,i) {
+                    if (!interactive) return;
+                    d3.select(this).classed('hover', false);
+                    dispatch.elementMouseout({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                })
+                .on('mousemove', function(d,i) {
+                    if (!interactive) return;
+                    dispatch.elementMousemove({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                })
+                .on('click', function(d,i) {
+                    if (!interactive) return;
+                    var element = this;
+                    dispatch.elementClick({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill"),
+                        event: d3.event,
+                        element: element
+                    });
+                    d3.event.stopPropagation();
+                })
+                .on('dblclick', function(d,i) {
+                    if (!interactive) return;
+                    dispatch.elementDblClick({
+                        data: d,
+                        index: i,
+                        color: d3.select(this).style("fill")
+                    });
+                    d3.event.stopPropagation();
+                });
+
+            bars
+                .attr('fill', function(d,i) { return color(d, i); })
+                .attr('class', function(d,i,j) { return (getY(d,i) < 0 ? 'nv-bar negative' : 'nv-bar positive') + ' nv-bar-' + j + '-' + i })
+                .watchTransition(renderWatch, 'bars')
+                .attr('transform', function(d,i) { return 'translate(' + (x(getX(d,i)) - availableWidth / data[0].values.length * .45) + ',0)'; })
+                //TODO: better width calculations that don't assume always uniform data spacing;w
+                .attr('width', (availableWidth / data[0].values.length) * .9 );
+
+            bars.watchTransition(renderWatch, 'bars')
+                .attr('y', function(d,i) {
+                    var rval = getY(d,i) < 0 ?
+                        y(0) :
+                            y(0) - y(getY(d,i)) < 1 ?
+                        y(0) - 1 :
+                        y(getY(d,i));
+                    return nv.utils.NaNtoZero(rval);
+                })
+                .attr('height', function(d,i) { return nv.utils.NaNtoZero(Math.max(Math.abs(y(getY(d,i)) - y(0)),1)) });
+
+        });
+
+        renderWatch.renderEnd('historicalBar immediate');
+        return chart;
+    }
+
+    //Create methods to allow outside functions to highlight a specific bar.
+    chart.highlightPoint = function(pointIndex, isHoverOver) {
+        container
+            .select(".nv-bars .nv-bar-0-" + pointIndex)
+            .classed("hover", isHoverOver)
+        ;
+    };
+
+    chart.clearHighlights = function() {
+        container
+            .select(".nv-bars .nv-bar.hover")
+            .classed("hover", false)
+        ;
+    };
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    chart.dispatch = dispatch;
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:   {get: function(){return width;}, set: function(_){width=_;}},
+        height:  {get: function(){return height;}, set: function(_){height=_;}},
+        forceX:  {get: function(){return forceX;}, set: function(_){forceX=_;}},
+        forceY:  {get: function(){return forceY;}, set: function(_){forceY=_;}},
+        padData: {get: function(){return padData;}, set: function(_){padData=_;}},
+        x:       {get: function(){return getX;}, set: function(_){getX=_;}},
+        y:       {get: function(){return getY;}, set: function(_){getY=_;}},
+        xScale:  {get: function(){return x;}, set: function(_){x=_;}},
+        yScale:  {get: function(){return y;}, set: function(_){y=_;}},
+        xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
+        yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
+        xRange:  {get: function(){return xRange;}, set: function(_){xRange=_;}},
+        yRange:  {get: function(){return yRange;}, set: function(_){yRange=_;}},
+        clipEdge:    {get: function(){return clipEdge;}, set: function(_){clipEdge=_;}},
+        id:          {get: function(){return id;}, set: function(_){id=_;}},
+        interactive: {get: function(){return interactive;}, set: function(_){interactive=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            margin.top    = _.top    !== undefined ? _.top    : margin.top;
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+        }}
+    });
+
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+
+nv.models.historicalBarChart = function(bar_model) {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var bars = bar_model || nv.models.historicalBar()
+        , xAxis = nv.models.axis()
+        , yAxis = nv.models.axis()
+        , legend = nv.models.legend()
+        , interactiveLayer = nv.interactiveGuideline()
+        , tooltip = nv.models.tooltip()
+        ;
+
+
+    var margin = {top: 30, right: 90, bottom: 50, left: 90}
+        , marginTop = null
+        , color = nv.utils.defaultColor()
+        , width = null
+        , height = null
+        , showLegend = false
+        , showXAxis = true
+        , showYAxis = true
+        , rightAlignYAxis = false
+        , useInteractiveGuideline = false
+        , x
+        , y
+        , state = {}
+        , defaultState = null
+        , noData = null
+        , dispatch = d3.dispatch('tooltipHide', 'stateChange', 'changeState', 'renderEnd')
+        , transitionDuration = 250
+        ;
+
+    xAxis.orient('bottom').tickPadding(7);
+    yAxis.orient( (rightAlignYAxis) ? 'right' : 'left');
+    tooltip
+        .duration(0)
+        .headerEnabled(false)
+        .valueFormatter(function(d, i) {
+            return yAxis.tickFormat()(d, i);
+        })
+        .headerFormatter(function(d, i) {
+            return xAxis.tickFormat()(d, i);
+        });
+
+
+    //============================================================
+    // Private Variables
+    //------------------------------------------------------------
+
+    var renderWatch = nv.utils.renderWatch(dispatch, 0);
+
+    function chart(selection) {
+        selection.each(function(data) {
+            renderWatch.reset();
+            renderWatch.models(bars);
+            if (showXAxis) renderWatch.models(xAxis);
+            if (showYAxis) renderWatch.models(yAxis);
+
+            var container = d3.select(this),
+                that = this;
+            nv.utils.initSVG(container);
+            var availableWidth = nv.utils.availableWidth(width, container, margin),
+                availableHeight = nv.utils.availableHeight(height, container, margin);
+
+            chart.update = function() { container.transition().duration(transitionDuration).call(chart) };
+            chart.container = this;
+
+            //set state.disabled
+            state.disabled = data.map(function(d) { return !!d.disabled });
+
+            if (!defaultState) {
+                var key;
+                defaultState = {};
+                for (key in state) {
+                    if (state[key] instanceof Array)
+                        defaultState[key] = state[key].slice(0);
+                    else
+                        defaultState[key] = state[key];
+                }
+            }
+
+            // Display noData message if there's nothing to show.
+            if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) {
+                nv.utils.noData(chart, container)
+                return chart;
+            } else {
+                container.selectAll('.nv-noData').remove();
+            }
+
+            // Setup Scales
+            x = bars.xScale();
+            y = bars.yScale();
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-wrap.nv-historicalBarChart').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-historicalBarChart').append('g');
+            var g = wrap.select('g');
+
+            gEnter.append('g').attr('class', 'nv-x nv-axis');
+            gEnter.append('g').attr('class', 'nv-y nv-axis');
+            gEnter.append('g').attr('class', 'nv-barsWrap');
+            gEnter.append('g').attr('class', 'nv-legendWrap');
+            gEnter.append('g').attr('class', 'nv-interactive');
+
+            // Legend
+            if (!showLegend) {
+                g.select('.nv-legendWrap').selectAll('*').remove();
+            } else {
+                legend.width(availableWidth);
+
+                g.select('.nv-legendWrap')
+                    .datum(data)
+                    .call(legend);
+
+                if (!marginTop && legend.height() !== margin.top) {
+                    margin.top = legend.height();
+                    availableHeight = nv.utils.availableHeight(height, container, margin);
+                }
+
+                wrap.select('.nv-legendWrap')
+                    .attr('transform', 'translate(0,' + (-margin.top) +')')
+            }
+            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            if (rightAlignYAxis) {
+                g.select(".nv-y.nv-axis")
+                    .attr("transform", "translate(" + availableWidth + ",0)");
+            }
+
+            //Set up interactive layer
+            if (useInteractiveGuideline) {
+                interactiveLayer
+                    .width(availableWidth)
+                    .height(availableHeight)
+                    .margin({left:margin.left, top:margin.top})
+                    .svgContainer(container)
+                    .xScale(x);
+                wrap.select(".nv-interactive").call(interactiveLayer);
+            }
+            bars
+                .width(availableWidth)
+                .height(availableHeight)
+                .color(data.map(function(d,i) {
+                    return d.color || color(d, i);
+                }).filter(function(d,i) { return !data[i].disabled }));
+
+            var barsWrap = g.select('.nv-barsWrap')
+                .datum(data.filter(function(d) { return !d.disabled }));
+            barsWrap.transition().call(bars);
+
+            // Setup Axes
+            if (showXAxis) {
+                xAxis
+                    .scale(x)
+                    ._ticks( nv.utils.calcTicksX(availableWidth/100, data) )
+                    .tickSize(-availableHeight, 0);
+
+                g.select('.nv-x.nv-axis')
+                    .attr('transform', 'translate(0,' + y.range()[0] + ')');
+                g.select('.nv-x.nv-axis')
+                    .transition()
+                    .call(xAxis);
+            }
+
+            if (showYAxis) {
+                yAxis
+                    .scale(y)
+                    ._ticks( nv.utils.calcTicksY(availableHeight/36, data) )
+                    .tickSize( -availableWidth, 0);
+
+                g.select('.nv-y.nv-axis')
+                    .transition()
+                    .call(yAxis);
+            }
+
+            //============================================================
+            // Event Handling/Dispatching (in chart's scope)
+            //------------------------------------------------------------
+
+            interactiveLayer.dispatch.on('elementMousemove', function(e) {
+                bars.clearHighlights();
+
+                var singlePoint, pointIndex, pointXLocation, allData = [];
+                data
+                    .filter(function(series, i) {
+                        series.seriesIndex = i;
+                        return !series.disabled;
+                    })
+                    .forEach(function(series,i) {
+                        pointIndex = nv.interactiveBisect(series.values, e.pointXValue, chart.x());
+                        bars.highlightPoint(pointIndex,true);
+                        var point = series.values[pointIndex];
+                        if (point === undefined) return;
+                        if (singlePoint === undefined) singlePoint = point;
+                        if (pointXLocation === undefined) pointXLocation = chart.xScale()(chart.x()(point,pointIndex));
+                        allData.push({
+                            key: series.key,
+                            value: chart.y()(point, pointIndex),
+                            color: color(series,series.seriesIndex),
+                            data: series.values[pointIndex]
+                        });
+                    });
+
+                var xValue = xAxis.tickFormat()(chart.x()(singlePoint,pointIndex));
+                interactiveLayer.tooltip
+                    .valueFormatter(function(d,i) {
+                        return yAxis.tickFormat()(d);
+                    })
+                    .data({
+                        value: xValue,
+                        index: pointIndex,
+                        series: allData
+                    })();
+
+                interactiveLayer.renderGuideLine(pointXLocation);
+
+            });
+
+            interactiveLayer.dispatch.on("elementMouseout",function(e) {
+                dispatch.tooltipHide();
+                bars.clearHighlights();
+            });
+
+            legend.dispatch.on('legendClick', function(d,i) {
+                d.disabled = !d.disabled;
+
+                if (!data.filter(function(d) { return !d.disabled }).length) {
+                    data.map(function(d) {
+                        d.disabled = false;
+                        wrap.selectAll('.nv-series').classed('disabled', false);
+                        return d;
+                    });
+                }
+
+                state.disabled = data.map(function(d) { return !!d.disabled });
+                dispatch.stateChange(state);
+
+                selection.transition().call(chart);
+            });
+
+            legend.dispatch.on('legendDblclick', function(d) {
+                //Double clicking should always enable current series, and disabled all others.
+                data.forEach(function(d) {
+                    d.disabled = true;
+                });
+                d.disabled = false;
+
+                state.disabled = data.map(function(d) { return !!d.disabled });
+                dispatch.stateChange(state);
+                chart.update();
+            });
+
+            dispatch.on('changeState', function(e) {
+                if (typeof e.disabled !== 'undefined') {
+                    data.forEach(function(series,i) {
+                        series.disabled = e.disabled[i];
+                    });
+
+                    state.disabled = e.disabled;
+                }
+
+                chart.update();
+            });
+        });
+
+        renderWatch.renderEnd('historicalBarChart immediate');
+        return chart;
+    }
+
+    //============================================================
+    // Event Handling/Dispatching (out of chart's scope)
+    //------------------------------------------------------------
+
+    bars.dispatch.on('elementMouseover.tooltip', function(evt) {
+        evt['series'] = {
+            key: chart.x()(evt.data),
+            value: chart.y()(evt.data),
+            color: evt.color
+        };
+        tooltip.data(evt).hidden(false);
+    });
+
+    bars.dispatch.on('elementMouseout.tooltip', function(evt) {
+        tooltip.hidden(true);
+    });
+
+    bars.dispatch.on('elementMousemove.tooltip', function(evt) {
+        tooltip();
+    });
+
+    //============================================================
+    // Expose Public Variables
+    //------------------------------------------------------------
+
+    // expose chart's sub-components
+    chart.dispatch = dispatch;
+    chart.bars = bars;
+    chart.legend = legend;
+    chart.xAxis = xAxis;
+    chart.yAxis = yAxis;
+    chart.interactiveLayer = interactiveLayer;
+    chart.tooltip = tooltip;
+
+    chart.options = nv.utils.optionsFunc.bind(chart);
+
+    chart._options = Object.create({}, {
+        // simple options, just get/set the necessary values
+        width:      {get: function(){return width;}, set: function(_){width=_;}},
+        height:     {get: function(){return height;}, set: function(_){height=_;}},
+        showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}},
+        showXAxis: {get: function(){return showXAxis;}, set: function(_){showXAxis=_;}},
+        showYAxis: {get: function(){return showYAxis;}, set: function(_){showYAxis=_;}},
+        defaultState:    {get: function(){return defaultState;}, set: function(_){defaultState=_;}},
+        noData:    {get: function(){return noData;}, set: function(_){noData=_;}},
+
+        // options that require extra logic in the setter
+        margin: {get: function(){return margin;}, set: function(_){
+            if (_.top !== undefined) {
+                margin.top = _.top;
+                marginTop = _.top;
+            }
+            margin.right  = _.right  !== undefined ? _.right  : margin.right;
+            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+            margin.left   = _.left   !== undefined ? _.left   : margin.left;
+        }},
+        color:  {get: function(){return color;}, set: function(_){
+            color = nv.utils.getColor(_);
+            legend.color(color);
+            bars.color(color);
+        }},
+        duration:    {get: function(){return transitionDuration;}, set: function(_){
+            transitionDuration=_;
+            renderWatch.reset(transitionDuration);
+            yAxis.duration(transitionDuration);
+            xAxis.duration(transitionDuration);
+        }},
+        rightAlignYAxis: {get: function(){return rightAlignYAxis;}, set: function(_){
+            rightAlignYAxis = _;
+            yAxis.orient( (_) ? 'right' : 'left');
+        }},
+        useInteractiveGuideline: {get: function(){return useInteractiveGuideline;}, set: function(_){
+            useInteractiveGuideline = _;
+            if (_ === true) {
+                chart.interactive(false);
+            }
+        }}
+    });
+
+    nv.utils.inheritOptions(chart, bars);
+    nv.utils.initOptions(chart);
+
+    return chart;
+};
+
+
+// ohlcChart is just a historical chart with ohlc bars and some tweaks
+nv.models.ohlcBarChart = function() {
+    var chart = nv.models.historicalBarChart(nv.models.ohlcBar());
+
+    // special default tooltip since we show multiple values per x
+    chart.useInteractiveGuideline(true);
+    chart.interactiveLayer.tooltip.contentGenerator(function(data) {
+        // we assume only one series exists for this chart
+        var d = data.series[0].data;
+        // match line colors as defined in nv.d3.css
+        var color = d.open < d.close ? "2ca02c" : "d62728";
+        return '' +
+            '<h3 style="color: #' + color + '">' + data.value + '</h3>' +
+            '<table>' +
+            '<tr><td>open:</td><td>' + chart.yAxis.tickFormat()(d.open) + '</td></tr>' +
+            '<tr><td>close:</td><td>' + chart.yAxis.tickFormat()(d.close) + '</td></tr>' +
+            '<tr><td>high</td><td>' + chart.yAxis.tickFormat()(d.high) + '</td></tr>' +
+            '<tr><td>low:</td><td>' + chart.yAxis.tickFormat()(d.low) + '</td></tr>' +
+            '</table>';
+    });
+    return chart;
+};
+
+// candlestickChart is just a historical chart with candlestick bars and some tweaks
+nv.models.candlestickBarChart = function() {
+    var chart = nv.models.historicalBarChart(nv.models.candlestickBar());
+
+    // special default tooltip since we show multiple values per x
+    chart.useInteractiveGuideline(true);
+    chart.interactiveLayer.tooltip.contentGenerator(function(data) {
+        // we assume only one series exists for this chart
+        var d = data.series[0].data;
+        // match line colors as defined in nv.d3.css
+        var color = d.open < d.close ? "2ca02c" : "d62728";
+        return '' +
+            '<h3 style="color: #' + color + '">' + data.value + '</h3>' +
+            '<table>' +
+            '<tr><td>open:</td><td>' + chart.yAxis.tickFormat()(d.open) + '</td></tr>' +
+            '<tr><td>close:</td><td>' + chart.yAxis.tickFormat()(d.close) + '</td></tr>' +
+            '<tr><td>high</td><td>' + chart.yAxis.tickFormat()(d.high) + '</td></tr>' +
+            '<tr><td>low:</td><td>' + chart.yAxis.tickFormat()(d.low) + '</td></tr>' +
+            '</table>';
+    });
+    return chart;
+};
+nv.models.legend = function() {
+    "use strict";
+
+    //============================================================
+    // Public Variables with Default Settings
+    //------------------------------------------------------------
+
+    var margin = {top: 5, right: 0, bottom: 5, left: 0}
+        , width = 400
+        , height = 20
+        , getKey = function(d) { return d.key }
+        , keyFormatter = function (d) { return d }
+        , color = nv.utils.getColor()
+        , maxKeyLength = 20 //default value for key lengths
+        , align = true
+        , padding = 32 //define how much space between legend items. - recommend 32 for furious version
+        , rightAlign = true
+        , updateState = true   //If true, legend will update data.disabled and trigger a 'stateChange' dispatch.
+        , enableDoubleClick = true   //If true, legend will enable double click handling
+        , radioButtonMode = false   //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time)
+        , expanded = false
+        , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange')
+        , vers = 'classic' //Options are "classic" and "furious"
+        ;
+
+    function chart(selection) {
+        selection.each(function(data) {
+            var availableWidth = width - margin.left - margin.right,
+                container = d3.select(this);
+            nv.utils.initSVG(container);
+
+            // Setup containers and skeleton of chart
+            var wrap = container.selectAll('g.nv-legend').data([data]);
+            var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g');
+            var g = wrap.select('g');
+
+            if (rightAlign)
+                wrap.attr('transform', 'translate(' + (- margin.right) + ',' + margin.top + ')');
+            else
+                wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+            var series = g.selectAll('.nv-series')
+                .data(function(d) {
+                    if(vers != 'furious') return d;
+
+                    return d.filter(function(n) {
+                        return expanded ? true : !n.disengaged;
+                    });
+                });
+
+            var seriesEnter = series.enter().append('g').attr('class', 'nv-series');
+            var seriesShape;
+
+            var versPadding;
+            switch(vers) {
+                case 'furious' :
+                    versPadding = 23;
+                    break;
+                case 'classic' :
+                    versPadding = 20;
+            }
+
+            if(vers == 'classic') {
+                seriesEnter.append('circle')
+                    .style('stroke-width', 2)
+                    .attr('class','nv-legend-symbol')
+                    .attr('r', 5);
+
+                seriesShape = series.select('.nv-legend-symbol');
+            } else if (vers == 'furious') {
+                seriesEnter.append('rect')
+                    .style('stroke-width', 2)
+                    .attr('class','nv-legend-symbol')
+                    .attr('rx', 3)
+                    .attr('ry', 3);
+                seriesShape = series.select('.nv-legend-symbol');
+
+                seriesEnter.append('g')
+                    .attr('class', 'nv-check-box')
+                    .property('innerHTML','<path d="M0.5,5 L22.5,5 L22.5,26.5 L0.5,26.5 L0.5,5 Z" class="nv-box"></path><path d="M5.5,12.8618467 L11.9185089,19.2803556 L31,0.198864511" class="nv-check"></path>')
+                    .attr('transform', 'translate(-10,-8)scale(0.5)');
+
+                var seriesCheckbox = series.select('.nv-check-box');
+
+                seriesCheckbox.each(function(d,i) {
+                    d3.select(this).selectAll('path')
+                        .attr('stroke', setTextColor(d,i));
+                });
+            }
+
+            seriesEnter.append('text')
+                .attr('text-anchor', 'start')
+                .attr('class','nv-legend-text')
+                .attr('dy', '.32em')
+                .attr('dx', '8');
+
+            var seriesText = series.select('text.nv-legend-text');
+
+            series
+                .on('mouseover', function(d,i) {
+                    dispatch.legendMouseover(d,i);  //TODO: Make consistent with other event objects
+                })
+                .on('mouseout', function(d,i) {
+                    dispatch.legendMouseout(d,i);
+                })
+                .on('click', function(d,i) {
+                    dispatch.legendClick(d,i);
... 28837 lines suppressed ...

[atlas] 01/02: ATLAS-4370: Persist Metrics for user to retrieve Metrics info at a past timestamp

Posted by sa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sarath pushed a commit to branch branch-2.0
in repository https://gitbox.apache.org/repos/asf/atlas.git

commit 447efb8e61b6604c2c4ba34991a18e78c0906432
Author: JP Li <jp...@8634.local>
AuthorDate: Thu Dec 2 01:15:53 2021 -0500

    ATLAS-4370: Persist Metrics for user to retrieve Metrics info at a past timestamp
    
    Co-authored-by: Disha Talreja <di...@cloudera.com>
    
    Signed-off-by: Sarath Subramanian <sa...@apache.org>
    (cherry picked from commit a4ee6c05ad6e8759cf2945024684bfbd1e7af8e1)
---
 addons/models/0000-Area0/0010-base_model.json      |  42 ++++
 .../java/org/apache/atlas/AtlasConfiguration.java  |   4 +-
 .../main/java/org/apache/atlas/AtlasErrorCode.java |   1 +
 .../model/metrics/AtlasMetricsMapToChart.java      |  62 +++++
 .../atlas/model/metrics/AtlasMetricsStat.java      | 159 +++++++++++++
 .../org/apache/atlas/utils/AtlasEntityUtil.java    |  50 ++++
 .../repository/graph/GraphBackedSearchIndexer.java |   7 +
 .../ogm/metrics/AtlasMetricsStatDTO.java           | 153 ++++++++++++
 .../org/apache/atlas/services/MetricsService.java  | 264 ++++++++++++++++++++-
 .../test/java/org/apache/atlas/TestModules.java    |   2 +
 .../apache/atlas/services/MetricsServiceTest.java  |  78 +++++-
 .../resources/solr/core-template/solrconfig.xml    |   2 +-
 .../apache/atlas/web/resources/AdminResource.java  | 196 ++++++++++++++-
 13 files changed, 1001 insertions(+), 19 deletions(-)

diff --git a/addons/models/0000-Area0/0010-base_model.json b/addons/models/0000-Area0/0010-base_model.json
index 769d885..a4a9248 100644
--- a/addons/models/0000-Area0/0010-base_model.json
+++ b/addons/models/0000-Area0/0010-base_model.json
@@ -371,6 +371,48 @@
       ]
     },
     {
+      "name": "__AtlasMetricsStat",
+      "superTypes": [
+        "__internal"
+      ],
+      "serviceType": "atlas_core",
+      "typeVersion": "1.0",
+      "attributeDefs": [
+        {
+          "name": "metricsId",
+          "typeName": "string",
+          "isOptional": false,
+          "cardinality": "SINGLE",
+          "isUnique": true,
+          "isIndexable": true
+        },
+        {
+          "name": "metrics",
+          "typeName": "string",
+          "isOptional": true,
+          "cardinality": "SINGLE",
+          "isUnique": false,
+          "isIndexable": false
+        },
+        {
+          "name": "collectionTime",
+          "typeName": "long",
+          "isOptional": true,
+          "cardinality": "SINGLE",
+          "isUnique": false,
+          "isIndexable": true
+        },
+        {
+          "name": "timeToLiveMillis",
+          "typeName": "long",
+          "isOptional": true,
+          "cardinality": "SINGLE",
+          "isUnique": false,
+          "isIndexable": true
+        }
+      ]
+    },
+    {
       "name": "__ExportImportAuditEntry",
       "serviceType": "atlas_core",
       "typeVersion": "1.0",
diff --git a/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java b/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java
index b63fab7..28fb68a 100644
--- a/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java
+++ b/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java
@@ -82,8 +82,8 @@ public enum AtlasConfiguration {
     DEBUG_METRICS_ENABLED("atlas.debug.metrics.enabled", false),
     TASKS_USE_ENABLED("atlas.tasks.enabled", true),
     SESSION_TIMEOUT_SECS("atlas.session.timeout.secs", -1),
-    UPDATE_COMPOSITE_INDEX_STATUS("atlas.update.composite.index.status", true);
-
+    UPDATE_COMPOSITE_INDEX_STATUS("atlas.update.composite.index.status", true),
+    METRICS_TIME_TO_LIVE_HOURS( "atlas.metrics.ttl.hours", 336); // 14 days default
 
     private static final Configuration APPLICATION_PROPERTIES;
 
diff --git a/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java b/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java
index 7d09261..8bc7996 100644
--- a/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java
+++ b/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java
@@ -211,6 +211,7 @@ public enum AtlasErrorCode {
     GLOSSARY_TERM_ALREADY_EXISTS(409, "ATLAS-409-00-009", "Glossary term with qualifiedName {0} already exists"),
     GLOSSARY_CATEGORY_ALREADY_EXISTS(409, "ATLAS-409-00-00A", "Glossary category with qualifiedName {0} already exists"),
     GLOSSARY_IMPORT_FAILED(409, "ATLAS-409-00-011", "Glossary import failed"),
+    METRICSSTAT_ALREADY_EXISTS(409, "ATLAS-409-00-012", "Metric Statistics already collected at {0}"),
 
     // All internal errors go here
     INTERNAL_ERROR(500, "ATLAS-500-00-001", "Internal server error {0}"),
diff --git a/intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsMapToChart.java b/intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsMapToChart.java
new file mode 100644
index 0000000..6e74f65
--- /dev/null
+++ b/intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsMapToChart.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.model.metrics;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+import java.util.List;
+
+import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
+import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.PUBLIC_ONLY;
+
+/**
+ * AtlasMetricsForChart is a formatted data type specifically for rendering the Stacked Area Chart.
+ * The Stacked Area Chart takes three String values as "key". For the Atlas Metrics entity, they are "Active", "Deleted", and "Shell".
+ * The Stacked Area Chart also takes a list of pairs (primitive values) as "values".
+ * The first element in the pair is collectionTime. It is used for rendering x-axis of the chart.
+ * The second element is the Atlas Metrics entity's count. It is used for rendering y-axis of the chart.
+ */
+@JsonAutoDetect(getterVisibility = PUBLIC_ONLY, setterVisibility = PUBLIC_ONLY, fieldVisibility = NONE)
+@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AtlasMetricsMapToChart {
+    private String key;
+    private List<long[]> values;
+
+    public AtlasMetricsMapToChart(String key, List<long[]> values) {
+        this.key    = key;
+        this.values = values;
+    }
+
+    public String getKey() { return key; }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public List<long[]> getValues() {
+        return values;
+    }
+
+    public void setValues(List<long[]> values) {
+        this.values = values;
+    }
+
+}
\ No newline at end of file
diff --git a/intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsStat.java b/intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsStat.java
new file mode 100644
index 0000000..5a30cad
--- /dev/null
+++ b/intg/src/main/java/org/apache/atlas/model/metrics/AtlasMetricsStat.java
@@ -0,0 +1,159 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.model.metrics;
+
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.apache.atlas.AtlasConfiguration;
+import org.apache.atlas.model.AtlasBaseModelObject;
+import org.apache.atlas.utils.AtlasEntityUtil;
+import org.apache.commons.collections.CollectionUtils;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
+import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.PUBLIC_ONLY;
+
+
+/**
+ * Atlas MetricsStat which includes Metrics' collection time and time to live (TTL).
+ */
+@JsonAutoDetect(getterVisibility = PUBLIC_ONLY, setterVisibility = PUBLIC_ONLY, fieldVisibility = NONE)
+@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AtlasMetricsStat extends AtlasBaseModelObject implements Serializable {
+
+    public static final String METRICS_CATEGORY_GENERAL_PROPERTY  = "general";
+    public static final String METRICS_COLLECTION_TIME_PROPERTY   = "collectionTime";
+    public static final String METRICS_ID_PREFIX_PROPERTY         = "atlas_metrics_";
+
+    private String       metricsId;
+    private long         collectionTime;
+    private long         timeToLiveMillis;
+
+    private Map<String, Object> typeData;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private AtlasMetrics metrics;
+
+    public AtlasMetricsStat() {
+    }
+
+    public AtlasMetricsStat(String guid) {
+        setGuid(guid);
+    }
+
+    public AtlasMetricsStat(AtlasMetrics metrics){
+        this(metrics, null);
+    }
+
+    public AtlasMetricsStat(AtlasMetrics metrics,List<String> listOfTypeNames) {
+        this(metrics, TimeUnit.HOURS.toMillis(AtlasConfiguration.METRICS_TIME_TO_LIVE_HOURS.getInt()),listOfTypeNames);
+
+    }
+
+    public AtlasMetricsStat(AtlasMetrics metrics, long timeToLiveMillis, List<String> listOfTypeNames) {
+        collectionTime = metrics == null ?
+                System.currentTimeMillis() : (long) metrics.getMetric(METRICS_CATEGORY_GENERAL_PROPERTY, METRICS_COLLECTION_TIME_PROPERTY);
+        setCollectionTime(collectionTime);
+
+        setMetricsId(METRICS_ID_PREFIX_PROPERTY + getCollectionTime() + "@" + AtlasEntityUtil.getMetadataNamespace());
+
+        setTimeToLiveMillis(timeToLiveMillis);
+        setMetrics(metrics);
+        setGuid(getGuid());
+
+        this.typeData = CollectionUtils.isEmpty(listOfTypeNames) ? null : new HashMap<>();
+        AtlasEntityUtil.metricsToTypeData(metrics, listOfTypeNames, typeData);
+    }
+
+
+
+    public String getMetricsId() {
+        return metricsId;
+    }
+
+    public void setMetricsId(String metricsId) {
+        this.metricsId = metricsId;
+    }
+
+    public long getCollectionTime() {
+        return collectionTime;
+    }
+
+    public void setCollectionTime(long collectionTime) {
+        this.collectionTime = collectionTime;
+    }
+
+    public long getTimeToLiveMillis() {
+        return timeToLiveMillis;
+    }
+
+    public void setTimeToLiveMillis(long timeToLiveMillis) {
+        this.timeToLiveMillis = timeToLiveMillis;
+    }
+
+    public AtlasMetrics getMetrics() {
+        return metrics;
+    }
+
+    public void setMetrics(AtlasMetrics metrics) {
+        this.metrics = metrics;
+    }
+
+    public Map<String, Object> getTypeData() {
+        return typeData;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+        AtlasMetricsStat that = (AtlasMetricsStat) o;
+        return Objects.equals(metrics, that.metrics);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), metrics);
+    }
+
+    @Override
+    protected StringBuilder toString(StringBuilder sb) {
+        sb.append(", metricsId=").append(metricsId);
+        sb.append(", collectionTime=").append(collectionTime);
+        sb.append(", timeToLiveMillis=").append(timeToLiveMillis);
+        sb.append(", metrics=");
+        if (metrics == null) {
+            sb.append("null");
+        } else {
+            sb.append(metrics);
+        }
+
+        return sb;
+    }
+}
\ No newline at end of file
diff --git a/intg/src/main/java/org/apache/atlas/utils/AtlasEntityUtil.java b/intg/src/main/java/org/apache/atlas/utils/AtlasEntityUtil.java
index 1e78e25..2b7846c 100644
--- a/intg/src/main/java/org/apache/atlas/utils/AtlasEntityUtil.java
+++ b/intg/src/main/java/org/apache/atlas/utils/AtlasEntityUtil.java
@@ -18,8 +18,11 @@
 package org.apache.atlas.utils;
 
 
+import org.apache.atlas.ApplicationProperties;
+import org.apache.atlas.AtlasException;
 import org.apache.atlas.model.instance.AtlasObjectId;
 import org.apache.atlas.model.instance.AtlasRelatedObjectId;
+import org.apache.atlas.model.metrics.AtlasMetrics;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang.StringUtils;
@@ -41,6 +44,15 @@ public class AtlasEntityUtil {
     private static final int    SOFT_REFERENCE_FORMAT_INDEX_TYPE_NAME = 0;
     private static final int    SOFT_REFERENCE_FORMAT_INDEX_GUID      = 1;
 
+    public static final String CONF_METADATA_NAMESPACE                = "atlas.metadata.namespace";
+    public static final String CLUSTER_NAME_KEY                       = "atlas.cluster.name";
+    public static final String DEFAULT_CLUSTER_NAME                   = "default";
+
+    protected static final String ENTITY                              = "entity";
+    protected static final String ACTIVE                              = "Active";
+    protected static final String DELETED                             = "Deleted";
+    protected static final String SHELL                               = "Shell";
+    protected static final String[] STATUS_CATEGORY                   = {ACTIVE, DELETED, SHELL};
 
     public static String formatSoftRefValue(String typeName, String guid) {
         return String.format(SOFT_REF_FORMAT, typeName, guid);
@@ -166,4 +178,42 @@ public class AtlasEntityUtil {
         return ret;
     }
 
+    public static String getMetadataNamespace() {
+        String ret = StringUtils.EMPTY;
+        try {
+            ret = ApplicationProperties.get().getString(CONF_METADATA_NAMESPACE, StringUtils.EMPTY);
+            if (StringUtils.isEmpty(ret)) {
+                ret = ApplicationProperties.get().getString(CLUSTER_NAME_KEY, DEFAULT_CLUSTER_NAME);
+            }
+        } catch (AtlasException e) {
+            LOG.info("Failed to load application properties", e);
+        }
+        return StringUtils.isNotEmpty(ret) ? ret : DEFAULT_CLUSTER_NAME;
+    }
+
+    public static void metricsToTypeData(AtlasMetrics metrics, String typeName, Map<String, Object> typeData) {
+        if (typeData == null) {
+            return;
+        }
+
+        Map<String, Integer> innerVal = new HashMap<>();
+
+        for (String status : STATUS_CATEGORY) {
+            Map<String, Integer> metricsMap = (Map<String, Integer>) metrics.getData().get(ENTITY).get(ENTITY + status);
+            innerVal.put(status, metricsMap.getOrDefault(typeName, 0));
+        }
+
+        typeData.put(typeName, innerVal);
+    }
+
+    public static void metricsToTypeData(AtlasMetrics metrics, List<String> typeNames, Map<String, Object> typeData) {
+        if (typeData == null || CollectionUtils.isEmpty(typeNames)) {
+            return;
+        }
+
+        for (String typeName : typeNames) {
+            metricsToTypeData(metrics, typeName, typeData);
+        }
+    }
+
 }
diff --git a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java
index d65bb1a..9924b2e 100755
--- a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java
+++ b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java
@@ -356,6 +356,13 @@ public class GraphBackedSearchIndexer implements SearchIndexer, ActiveStateChang
             // index recovery
             createCommonVertexIndex(management, PROPERTY_KEY_INDEX_RECOVERY_NAME, UniqueKind.GLOBAL_UNIQUE, String.class, SINGLE, true, false);
 
+            //metrics
+            createCommonVertexIndex(management," __AtlasMetricsStat.metricsId", UniqueKind.GLOBAL_UNIQUE, String.class, SINGLE, true, false);
+            createCommonVertexIndex(management," __AtlasMetricsStat.__u_metricsId", UniqueKind.GLOBAL_UNIQUE, String.class, SINGLE, true, false);
+            createCommonVertexIndex(management," __AtlasMetricsStat.metrics", UniqueKind.NONE, String.class, SINGLE, true, false);
+            createCommonVertexIndex(management," __AtlasMetricsStat.collectionTime", UniqueKind.GLOBAL_UNIQUE, String.class, SINGLE, true, false);
+            createCommonVertexIndex(management," __AtlasMetricsStat.timeToLiveMillis", UniqueKind.NONE, String.class, SINGLE, true, false);
+
             // create vertex-centric index
             createVertexCentricIndex(management, CLASSIFICATION_LABEL, AtlasEdgeDirection.BOTH, CLASSIFICATION_EDGE_NAME_PROPERTY_KEY, String.class, SINGLE);
             createVertexCentricIndex(management, CLASSIFICATION_LABEL, AtlasEdgeDirection.BOTH, CLASSIFICATION_EDGE_IS_PROPAGATED_PROPERTY_KEY, Boolean.class, SINGLE);
diff --git a/repository/src/main/java/org/apache/atlas/repository/ogm/metrics/AtlasMetricsStatDTO.java b/repository/src/main/java/org/apache/atlas/repository/ogm/metrics/AtlasMetricsStatDTO.java
new file mode 100644
index 0000000..6af935e
--- /dev/null
+++ b/repository/src/main/java/org/apache/atlas/repository/ogm/metrics/AtlasMetricsStatDTO.java
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.repository.ogm.metrics;
+
+import org.apache.atlas.exception.AtlasBaseException;
+import org.apache.atlas.model.instance.AtlasEntity;
+import org.apache.atlas.model.instance.AtlasEntity.AtlasEntityWithExtInfo;
+import org.apache.atlas.model.metrics.AtlasMetrics;
+import org.apache.atlas.model.metrics.AtlasMetricsStat;
+import org.apache.atlas.repository.impexp.AuditsWriter;
+import org.apache.atlas.repository.ogm.AbstractDataTransferObject;
+import org.apache.atlas.type.AtlasType;
+import org.apache.atlas.type.AtlasTypeRegistry;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.atlas.model.metrics.AtlasMetricsStat.METRICS_ID_PREFIX_PROPERTY;
+
+
+/**
+ * AtlasMetricsStatDTO is the bridge class in between AtlasMetricsStat and AtlasEntity.
+ */
+@Component
+public class AtlasMetricsStatDTO extends AbstractDataTransferObject<AtlasMetricsStat> {
+    private static final Logger LOG = LoggerFactory.getLogger(AtlasMetricsStatDTO.class);
+
+    public static final String METRICS_ENTITY_TYPE_NAME  = "__AtlasMetricsStat";
+    public static final String METRICS_ID_PROPERTY       = "metricsId";
+    private static final String METRICS_PROPERTY         = "metrics";
+    private static final String COLLECTION_TIME_PROPERTY = "collectionTime";
+    private static final String TIME_TO_LIVE_PROPERTY    = "timeToLiveMillis";
+    private static final String UNIQUE_NAME_PROPERTY     = "uniqueName";
+
+    @Inject
+    public AtlasMetricsStatDTO(AtlasTypeRegistry typeRegistry) {
+        super(typeRegistry, AtlasMetricsStat.class, METRICS_ENTITY_TYPE_NAME);
+    }
+
+    @Override
+    public AtlasMetricsStat from(AtlasEntity entity) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> AtlasMetricsStatDTO.from({})", entity);
+        }
+
+        AtlasMetricsStat metricsStat = null;
+
+        String jsonMetrics = (String) entity.getAttribute(METRICS_PROPERTY);
+
+        if (StringUtils.isNotEmpty(jsonMetrics)) {
+            metricsStat = new AtlasMetricsStat(AtlasType.fromJson(jsonMetrics, AtlasMetrics.class));
+        }
+
+        if (metricsStat == null) {
+            LOG.error("MetricStat cannot be created without metric info. Null has been returned.");
+        } else {
+            metricsStat.setGuid(entity.getGuid());
+            metricsStat.setMetricsId((String) entity.getAttribute(METRICS_ID_PROPERTY));
+
+            metricsStat.setCollectionTime((long) entity.getAttribute(COLLECTION_TIME_PROPERTY));
+
+            metricsStat.setTimeToLiveMillis((long) entity.getAttribute(TIME_TO_LIVE_PROPERTY));
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== AtlasMetricsStatDTO.from() : {}", metricsStat);
+        }
+
+        return metricsStat;
+    }
+
+    @Override
+    public AtlasMetricsStat from(AtlasEntity.AtlasEntityWithExtInfo entityWithExtInfo) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> AtlasMetricsStatDTO.from({})", entityWithExtInfo);
+        }
+
+        AtlasMetricsStat ret = from(entityWithExtInfo.getEntity());
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== AtlasMetricsStatDTO.from() : {}", ret);
+        }
+        return ret;
+    }
+
+    @Override
+    public AtlasEntity toEntity(AtlasMetricsStat obj) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> AtlasMetricsStatDTO.toEntity({})", obj);
+        }
+
+        AtlasEntity entity = getDefaultAtlasEntity(obj);
+
+        entity.setAttribute(METRICS_ID_PROPERTY, getUniqueValue(obj));
+
+        if (obj.getMetrics() != null) {
+            entity.setAttribute(METRICS_PROPERTY, AtlasType.toJson(obj.getMetrics()));
+        }
+
+        entity.setAttribute(COLLECTION_TIME_PROPERTY, obj.getCollectionTime());
+        entity.setAttribute(TIME_TO_LIVE_PROPERTY, obj.getTimeToLiveMillis());
+        entity.setAttribute(UNIQUE_NAME_PROPERTY, getUniqueValue(obj));
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== AtlasMetricsStatDTO.toEntity() : {}", entity);
+        }
+        return entity;
+    }
+
+    @Override
+    public AtlasEntityWithExtInfo toEntityWithExtInfo(AtlasMetricsStat obj) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> AtlasMetricsStatDTO.toEntityWithExtInfo({})", obj);
+        }
+        AtlasEntityWithExtInfo ret = new AtlasEntityWithExtInfo(toEntity(obj));
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== AtlasMetricsStatDTO.toEntityWithExtInfo() : {}", ret);
+        }
+        return ret;
+    }
+
+    @Override
+    public Map<String, Object> getUniqueAttributes(AtlasMetricsStat obj) {
+        Map<String, Object> ret = new HashMap<>();
+        ret.put(METRICS_ID_PROPERTY, getUniqueValue(obj));
+        return ret;
+    }
+
+    private String getUniqueValue(AtlasMetricsStat obj) {
+        return METRICS_ID_PREFIX_PROPERTY + obj.getCollectionTime() + "@" + AuditsWriter.getCurrentClusterName();
+    }
+}
diff --git a/repository/src/main/java/org/apache/atlas/services/MetricsService.java b/repository/src/main/java/org/apache/atlas/services/MetricsService.java
index ceb2528..9ec2cd2 100644
--- a/repository/src/main/java/org/apache/atlas/services/MetricsService.java
+++ b/repository/src/main/java/org/apache/atlas/services/MetricsService.java
@@ -17,40 +17,62 @@
  */
 package org.apache.atlas.services;
 
+import org.apache.atlas.AtlasErrorCode;
+import org.apache.atlas.SortOrder;
 import org.apache.atlas.annotation.AtlasService;
 import org.apache.atlas.annotation.GraphTransaction;
 import org.apache.atlas.authorize.AtlasAuthorizationUtils;
 import org.apache.atlas.authorize.AtlasTypesDefFilterRequest;
+import org.apache.atlas.exception.AtlasBaseException;
 import org.apache.atlas.model.instance.AtlasEntity.Status;
 import org.apache.atlas.model.metrics.AtlasMetrics;
+import org.apache.atlas.model.metrics.AtlasMetricsMapToChart;
+import org.apache.atlas.model.metrics.AtlasMetricsStat;
 import org.apache.atlas.model.typedef.AtlasClassificationDef;
 import org.apache.atlas.model.typedef.AtlasEntityDef;
 import org.apache.atlas.model.typedef.AtlasTypesDef;
 import org.apache.atlas.repository.Constants;
 import org.apache.atlas.repository.graphdb.AtlasGraph;
+import org.apache.atlas.repository.graphdb.AtlasVertex;
+import org.apache.atlas.repository.ogm.DataAccess;
 import org.apache.atlas.repository.store.graph.v2.AtlasGraphUtilsV2;
 import org.apache.atlas.type.AtlasEntityType;
 import org.apache.atlas.type.AtlasTypeRegistry;
 import org.apache.atlas.util.AtlasMetricJVMUtil;
 import org.apache.atlas.util.AtlasMetricsUtil;
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import static org.apache.atlas.discovery.SearchProcessor.AND_STR;
 import static org.apache.atlas.model.instance.AtlasEntity.Status.ACTIVE;
 import static org.apache.atlas.model.instance.AtlasEntity.Status.DELETED;
 import static org.apache.atlas.repository.Constants.*;
+import static org.apache.atlas.repository.ogm.metrics.AtlasMetricsStatDTO.METRICS_ENTITY_TYPE_NAME;
+import static org.apache.atlas.repository.ogm.metrics.AtlasMetricsStatDTO.METRICS_ID_PROPERTY;
 
 @AtlasService
 public class MetricsService {
     private static final Logger LOG = LoggerFactory.getLogger(MetricsService.class);
 
+    private final DataAccess dataAccess;
+
     // Query Category constants
     public static final String TYPE             = "type";
     public static final String TYPE_SUBTYPES    = "typeAndSubTypes";
@@ -76,6 +98,7 @@ public class MetricsService {
     protected static final String METRIC_ENTITY_ACTIVE_INCL_SUBTYPES = ENTITY + "Active"+"-"+TYPE_SUBTYPES;
     protected static final String METRIC_ENTITY_DELETED_INCL_SUBTYPES = ENTITY + "Deleted"+"-"+TYPE_SUBTYPES;
     protected static final String METRIC_ENTITY_SHELL_INCL_SUBTYPES = ENTITY + "Shell"+"-"+TYPE_SUBTYPES;
+    protected static final String[] STATUS_CATEGORY                 = {"Active", "Deleted", "Shell"};
 
     private final AtlasGraph        atlasGraph;
     private final AtlasTypeRegistry typeRegistry;
@@ -83,10 +106,12 @@ public class MetricsService {
     private final String            indexSearchPrefix = AtlasGraphUtilsV2.getIndexSearchPrefix();
 
     @Inject
-    public MetricsService(final AtlasGraph graph, final AtlasTypeRegistry typeRegistry, AtlasMetricsUtil metricsUtil) {
+    public MetricsService(final AtlasGraph graph, final AtlasTypeRegistry typeRegistry, AtlasMetricsUtil metricsUtil,
+                          DataAccess dataAccess) {
         this.atlasGraph   = graph;
         this.typeRegistry = typeRegistry;
         this.metricsUtil  = metricsUtil;
+        this.dataAccess   = dataAccess;
     }
 
     @SuppressWarnings("unchecked")
@@ -134,7 +159,6 @@ public class MetricsService {
                 }
             }
 
-
             for (AtlasEntityDef entityDef : entityDefs) {
                 AtlasEntityType entityType = typeRegistry.getEntityTypeByName(entityDef.getName());
 
@@ -262,4 +286,240 @@ public class MetricsService {
         return ret;
     }
 
+    public AtlasMetricsStat saveMetricsStat(AtlasMetricsStat metricsStat) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.saveMetricsStat({})", metricsStat);
+        }
+
+        if (Objects.isNull(metricsStat) || StringUtils.isEmpty(metricsStat.getMetricsId())) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, "MetricsStat definition missing.");
+        }
+
+        if (metricsStatExists(metricsStat)) {
+            throw new AtlasBaseException(AtlasErrorCode.METRICSSTAT_ALREADY_EXISTS, String.valueOf(metricsStat.getCollectionTime()));
+        }
+
+        AtlasMetricsStat storeObject = dataAccess.save(metricsStat);
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.saveMetricsStat() : {}", storeObject);
+        }
+
+        return storeObject;
+    }
+
+    public void purgeMetricsStats() throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.purgeMetricsStats()");
+        }
+
+        long currentTimeMillis = System.currentTimeMillis();
+
+        List<AtlasMetricsStat> metricsStats = getAllMetricsStats(true)
+                .stream()
+                .filter(c -> c.getCollectionTime() + c.getTimeToLiveMillis() < currentTimeMillis)
+                .collect(Collectors.toList());
+
+        for (AtlasMetricsStat a : metricsStats) {
+            long collectedTime = a.getCollectionTime();
+            deleteMetricsStatByCollectionTime(String.valueOf(collectedTime));
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.purgeMetricsStats() : {}", metricsStats);
+        }
+    }
+
+    @GraphTransaction
+    public AtlasMetricsStat getMetricsStatByCollectionTime(final String collectionTime) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.getMetricsStatByCollectionTime({})", collectionTime);
+        }
+
+        if (StringUtils.isBlank(collectionTime)) {
+            throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, "collectionTime is null/empty");
+        }
+
+        AtlasMetricsStat ret;
+
+        AtlasMetricsStat metricsStat = new AtlasMetricsStat();
+        metricsStat.setCollectionTime(Long.parseLong(collectionTime));
+
+        ret = dataAccess.load(metricsStat);
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.getMetricsStatByCollectionTime() : {}", ret);
+        }
+
+        return ret;
+    }
+
+    @GraphTransaction
+    public void deleteMetricsStatByCollectionTime(final String collectionTime) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.deleteMetricsStatByCollectionTime({})", collectionTime);
+        }
+
+        if (StringUtils.isEmpty(collectionTime)) {
+            throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, collectionTime);
+        }
+
+        AtlasMetricsStat deleteStat = getMetricsStatByCollectionTime(collectionTime);
+
+        dataAccess.delete(deleteStat.getGuid());
+
+        // delete log
+        if (LOG.isDebugEnabled()) {
+            long currTime      = System.currentTimeMillis();
+            long collectedTime = deleteStat.getCollectionTime();
+
+            LOG.info("MetricsService.deleteMetricsStatByCollectionTime(): At {}, metricsStat with collectionTime: {}, persisted hours: {}, is deleted. ",
+                    Instant.ofEpochMilli(currTime),
+                    Instant.ofEpochMilli(collectedTime),
+                    TimeUnit.MILLISECONDS.toHours(currTime - collectedTime)
+            );
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.deleteMetricsStatByCollectionTime({})", collectionTime);
+        }
+
+    }
+
+    @GraphTransaction
+    public List<AtlasMetricsStat> getAllMetricsStats(Boolean minInfo) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.getAllMetricsStats()");
+        }
+
+        List<AtlasMetricsStat> ret = new ArrayList<>();
+        // SortOrder.ASCENDING is a necessary input parameter. It only sorts GUIDs, but not collectionTime.
+        List<String> guids = AtlasGraphUtilsV2.findEntityGUIDsByType(METRICS_ENTITY_TYPE_NAME, SortOrder.ASCENDING);
+
+        if (CollectionUtils.isNotEmpty(guids)) {
+            List<AtlasMetricsStat> metricsToLoad = guids.stream()
+                    .map(AtlasMetricsStat::new)
+                    .collect(Collectors.toList());
+
+            Iterable<AtlasMetricsStat> metricsStats = dataAccess.load(metricsToLoad);
+
+
+            ret = StreamSupport.stream(metricsStats.spliterator(), false)
+                    .sorted((a, b) -> (int) (b.getCollectionTime() - a.getCollectionTime()))
+                    .map(m -> {
+                        if(minInfo) {
+                            m.setMetrics(null);
+                        }
+                        return m;
+                    }).collect(Collectors.toList());
+
+        } else {
+            ret = Collections.emptyList();
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.getAllMetricsStats() : {}", ret);
+        }
+
+        return ret;
+    }
+
+
+    public List<AtlasMetricsStat> getMetricsInRangeByTypeNames(long startTime,
+                                                               long endTime,
+                                                               List<String> typeNames) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.getMetricsInRangeByTypeNames({}, {}, {})", startTime, endTime, String.join(", ", typeNames));
+        }
+
+        if (startTime >= endTime) {
+            throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS,
+                    "startTime: '" + startTime + "', should be less than, endTime: '" + endTime + "'");
+        }
+
+        List<AtlasMetricsStat> metricsInRange;
+        List<AtlasMetricsStat> allMetrics = getAllMetricsStats(false);
+
+        metricsInRange = allMetrics.stream()
+                .filter(m -> m.getCollectionTime() >= startTime && m.getCollectionTime() <= endTime)
+                .map(m ->  {
+                    m = new AtlasMetricsStat(m.getMetrics(), typeNames);
+                    m.setMetrics(null);
+                    return m;
+                })
+                .collect(Collectors.toList());
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.getMetricsInRangeByTypeNames() : {}", metricsInRange);
+        }
+
+        return metricsInRange;
+    }
+
+    public Map<String, List<AtlasMetricsMapToChart>> getMetricsForChartByTypeNames(long         startTime,
+                                                                                   long         endTime,
+                                                                                   List<String> typeNames) throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> MetricsService.getMetricsForChartByTypeNames({}, {}, {})", startTime, endTime, typeNames);
+        }
+
+        // Calling getMetricsInRangeByTypeNames() and constructing AtlasMetricsStat with list of typeNames, to retrieve JanusGraph only once.
+        Map<String, List<AtlasMetricsMapToChart>> ret = new HashMap<>();
+
+        // Returned metrics were sorted by collectionTime descendingly. Reverse it to ascending order to match stacked area chart's required input format.
+        List<AtlasMetricsStat> metrics = getMetricsInRangeByTypeNames(startTime, endTime, typeNames);
+        Collections.reverse(metrics);
+
+        for (String typeName : typeNames) {
+            Map<String, List<long[]>> statusCategory = mapToStatusCategoryByOneType(metrics, typeName);
+
+            if (MapUtils.isNotEmpty(statusCategory)) {
+                ret.put(typeName, statusCategory.entrySet()
+                        .stream()
+                        .map(c -> new AtlasMetricsMapToChart(c.getKey(), c.getValue()))
+                        .collect(Collectors.toList())
+                );
+            } else {
+                LOG.info("MetricsService.getMetricsForChartByTypeNames() : data of typeName:{} cannot be found.", typeName);
+                ret.put(typeName, Collections.emptyList());
+            }
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== MetricsService.getMetricsForChartByTypeNames() : {}", ret);
+        }
+
+        return ret;
+    }
+
+    /** Mapping each typeName's counting info in AtlasMetricsStat to the required format to render the stacked area chart.
+     *  Keys:   3 categories: Active, Deleted & Shell.
+     *  Values: a list of pair with the first element as collectionTime, and the second element as count.
+     */
+    private Map<String, List<long[]>> mapToStatusCategoryByOneType(List<AtlasMetricsStat> metrics, String typeName) {
+        // Use LinkedHashMap to make sure the status are in order as Active, Deleted and Shell for rendering chart
+        Map<String, List<long[]>> statusCategory = new LinkedHashMap<>();
+
+        for (AtlasMetricsStat metric : metrics) {
+            Map<String, Integer> metricsMap = null;
+            if (metric.getTypeData() != null) {
+                metricsMap = (Map<String, Integer>) metric.getTypeData().get(typeName);
+            }
+
+            for (String status : STATUS_CATEGORY) {
+                long statusCnt = metricsMap == null? (long) 0: metricsMap.get(status);
+                statusCategory.computeIfAbsent(status, a -> new ArrayList<>()).add(new long[]{metric.getCollectionTime(), statusCnt});
+            }
+        }
+
+        return statusCategory;
+    }
+
+    private boolean metricsStatExists(AtlasMetricsStat metricsStat) {
+        AtlasVertex vertex = AtlasGraphUtilsV2.findByUniqueAttributes(typeRegistry.getEntityTypeByName(METRICS_ENTITY_TYPE_NAME), new HashMap<String, Object>() {{
+            put(METRICS_ID_PROPERTY, metricsStat.getMetricsId());
+        }});
+        return Objects.nonNull(vertex);
+    }
+
 }
\ No newline at end of file
diff --git a/repository/src/test/java/org/apache/atlas/TestModules.java b/repository/src/test/java/org/apache/atlas/TestModules.java
index 8dda208..a0a6354 100644
--- a/repository/src/test/java/org/apache/atlas/TestModules.java
+++ b/repository/src/test/java/org/apache/atlas/TestModules.java
@@ -52,6 +52,7 @@ import org.apache.atlas.repository.ogm.ExportImportAuditEntryDTO;
 import org.apache.atlas.repository.ogm.glossary.AtlasGlossaryCategoryDTO;
 import org.apache.atlas.repository.ogm.glossary.AtlasGlossaryDTO;
 import org.apache.atlas.repository.ogm.glossary.AtlasGlossaryTermDTO;
+import org.apache.atlas.repository.ogm.metrics.AtlasMetricsStatDTO;
 import org.apache.atlas.repository.ogm.profiles.AtlasSavedSearchDTO;
 import org.apache.atlas.repository.ogm.profiles.AtlasUserProfileDTO;
 import org.apache.atlas.repository.store.graph.AtlasEntityStore;
@@ -178,6 +179,7 @@ public class TestModules {
             availableDTOs.addBinding().to(AtlasServerDTO.class);
             availableDTOs.addBinding().to(ExportImportAuditEntryDTO.class);
             availableDTOs.addBinding().to(AtlasAuditEntryDTO.class);
+            availableDTOs.addBinding().to(AtlasMetricsStatDTO.class);
 
             bind(DTORegistry.class).asEagerSingleton();
             bind(DataAccess.class).asEagerSingleton();
diff --git a/repository/src/test/java/org/apache/atlas/services/MetricsServiceTest.java b/repository/src/test/java/org/apache/atlas/services/MetricsServiceTest.java
index 0405921..d114bf5 100644
--- a/repository/src/test/java/org/apache/atlas/services/MetricsServiceTest.java
+++ b/repository/src/test/java/org/apache/atlas/services/MetricsServiceTest.java
@@ -17,10 +17,12 @@
  */
 package org.apache.atlas.services;
 
+import org.apache.atlas.AtlasErrorCode;
 import org.apache.atlas.RequestContext;
 import org.apache.atlas.TestModules;
 import org.apache.atlas.exception.AtlasBaseException;
 import org.apache.atlas.model.metrics.AtlasMetrics;
+import org.apache.atlas.model.metrics.AtlasMetricsStat;
 import org.apache.atlas.repository.AtlasTestBase;
 import org.apache.atlas.repository.graph.AtlasGraphProvider;
 import org.apache.atlas.repository.impexp.ImportService;
@@ -46,18 +48,9 @@ import java.util.HashMap;
 import java.util.Map;
 
 import static org.apache.atlas.model.metrics.AtlasMetrics.*;
+import static org.apache.atlas.services.MetricsService.*;
 import static org.apache.atlas.utils.TestLoadModelUtils.loadModelFromJson;
 import static org.apache.atlas.repository.impexp.ZipFileResourceTestUtils.runImportWithNoParameters;
-import static org.apache.atlas.services.MetricsService.ENTITY;
-import static org.apache.atlas.services.MetricsService.GENERAL;
-import static org.apache.atlas.services.MetricsService.METRIC_ENTITIES_PER_TAG;
-import static org.apache.atlas.services.MetricsService.METRIC_ENTITY_ACTIVE;
-import static org.apache.atlas.services.MetricsService.METRIC_ENTITY_COUNT;
-import static org.apache.atlas.services.MetricsService.METRIC_ENTITY_DELETED;
-import static org.apache.atlas.services.MetricsService.METRIC_TAG_COUNT;
-import static org.apache.atlas.services.MetricsService.METRIC_TYPE_COUNT;
-import static org.apache.atlas.services.MetricsService.METRIC_TYPE_UNUSED_COUNT;
-import static org.apache.atlas.services.MetricsService.TAG;
 import static org.testng.Assert.*;
 
 @Guice(modules = TestModules.TestOnlyModule.class)
@@ -118,6 +111,9 @@ public class MetricsServiceTest extends AtlasTestBase {
         put(STAT_NOTIFY_FAILED_COUNT_PREV_DAY, 1L);
     }};
 
+    private AtlasMetrics metrics;
+    private AtlasMetricsStat blankMetricsStat, metricsStatInGraph;
+
     @BeforeClass
     public void setup() throws Exception {
         RequestContext.clear();
@@ -145,9 +141,9 @@ public class MetricsServiceTest extends AtlasTestBase {
         super.cleanup();
     }
 
-    @Test
+    @Test(groups = "Metrics.CREATE")
     public void testGetMetrics() {
-        AtlasMetrics metrics = metricsService.getMetrics();
+        metrics = metricsService.getMetrics();
 
         assertNotNull(metrics);
 
@@ -171,6 +167,64 @@ public class MetricsServiceTest extends AtlasTestBase {
         assertEquals(deletedEntityMetricsActual, deletedEntityMetricsExpected);
     }
 
+    @Test(groups = "Metrics.CREATE", dependsOnMethods = "testGetMetrics")
+    public void testSaveMetricsStat() {
+        try {
+            blankMetricsStat = new AtlasMetricsStat(metrics);
+            metricsStatInGraph = metricsService.saveMetricsStat(blankMetricsStat);
+        } catch (AtlasBaseException e) {
+            fail("Save metricsStat should've succeeded", e);
+        }
+
+        // Duplicate create calls should fail
+        try {
+            AtlasMetricsStat blankMetricsStatDup = new AtlasMetricsStat(metrics);
+            metricsService.saveMetricsStat(blankMetricsStatDup);
+            fail("Save duplicate metricsStat should've failed");
+        } catch (AtlasBaseException e) {
+            assertEquals(e.getAtlasErrorCode(), AtlasErrorCode.METRICSSTAT_ALREADY_EXISTS);
+        }
+    }
+
+    @Test(groups = "Metrics.CREATE", dependsOnMethods = "testSaveMetricsStat")
+    public void testGetMetricsStatByCollectionTime() {
+        // collectionTime is empty string
+        try {
+            AtlasMetricsStat metricsStatRet = metricsService.getMetricsStatByCollectionTime("  ");
+            fail("Get metricsStat by collectionTime should've failed, when collectionTime is empty.");
+        } catch (AtlasBaseException e) {
+            assertEquals(e.getAtlasErrorCode(), AtlasErrorCode.INVALID_PARAMETERS);
+        }
+
+        // collectionTime is null
+        try {
+            AtlasMetricsStat metricsStatRet = metricsService.getMetricsStatByCollectionTime(null);
+            fail("Get metricsStat by collectionTime should've failed, when collectionTime is null.");
+        } catch (AtlasBaseException e) {
+            assertEquals(e.getAtlasErrorCode(), AtlasErrorCode.INVALID_PARAMETERS);
+        }
+
+        // collectionTime is NOT existed
+        try {
+            Long collectionTimeInGraph = System.currentTimeMillis();
+            AtlasMetricsStat metricsStatRet = metricsService.getMetricsStatByCollectionTime(String.valueOf(collectionTimeInGraph));
+            fail("Get metricsStat by collectionTime should've failed, when collectionTime is NOT existed.");
+        } catch (AtlasBaseException e) {
+            assertEquals(e.getAtlasErrorCode(), AtlasErrorCode.INSTANCE_BY_UNIQUE_ATTRIBUTE_NOT_FOUND);
+        }
+
+        // collectionTime is correct
+        try {
+            Long collectionTimeInGraph = (Long) metrics.getMetric(GENERAL, METRIC_COLLECTION_TIME);
+            AtlasMetricsStat metricsStatRet = metricsService.getMetricsStatByCollectionTime(String.valueOf(collectionTimeInGraph));
+            assertNotNull(metricsStatRet);
+            assertEquals(metricsStatRet.getGuid(), metricsStatInGraph.getGuid());
+            assertEquals(metricsStatRet.getMetricsId(), metricsStatInGraph.getMetricsId());
+        } catch (AtlasBaseException e) {
+            fail("Get metricsStat by valid collectionTime in Graph should've succeeded.");
+        }
+    }
+
     @Test
     public void testNotificationMetrics() {
         Instant now           = Clock.systemUTC().instant();
diff --git a/test-tools/src/main/resources/solr/core-template/solrconfig.xml b/test-tools/src/main/resources/solr/core-template/solrconfig.xml
index 3341e71..fa38e72 100644
--- a/test-tools/src/main/resources/solr/core-template/solrconfig.xml
+++ b/test-tools/src/main/resources/solr/core-template/solrconfig.xml
@@ -445,7 +445,7 @@
          -->
         <lst name="defaults">
             <str name="defType">edismax</str>
-            <str name="qf">35x_t 5j9_t 7wl_t a9x_t but_t dfp_l f0l_t i6d_l iyt_l jr9_t kjp_s lc5_t m4l_s mx1_t ohx_t xz9_i 1151_t 12px_t 14at_l 15vp_t 1891_t 19tx_t 1bet_t 1czp_t 1ekl_t 1gxx_t 1iit_l 1k3p_t 1lol_t 3z0l_t 4bnp_s 4a2t_s 47ph_s 464l_s 49ad_t 4h6t_t 4d8l_t 4eth_l 4flx_t 4lxh_t 4kcl_t 4nid_t 4umd_t 505h_t 54w5_t 52it_t 53b9_t 51qd_t 5b7p_t 5af9_t 5j45_l 5kp1_l 5hj9_t 5fyd_t 5m9x_t 5c05_t 5csl_t 5edh_t 5wjp_t 5xc5_l 5tdx_t 5q85_l 5rt1_l 5n2d_t 5uyt_t 8mit_t 8emd_t 8o3p_t 8k5h_ [...]
+            <str name="qf">35x_t 5j9_t 7wl_t a9x_t but_t dfp_l f0l_t i6d_l iyt_l jr9_t kjp_s lc5_t m4l_s mx1_t ohx_t xz9_i 1151_t 12px_t 14at_l 15vp_t 1891_t 19tx_t 1bet_t 1czp_t 1ekl_t 1gxx_t 1iit_l 1k3p_t 1lol_t 1o1x_t 1qf9_t 1ssl_t 1udh_t 1wqt_t 4d8l_t 4pvp_s 4oat_s 4lxh_s 4kcl_s 4nid_t 4vet_t 4rgl_t 4t1h_l 4ttx_t 505h_t 4ykl_t 51qd_t 58ud_t 5edh_t 5j45_t 5gqt_t 5hj9_t 5fyd_t 5pfp_t 5on9_t 5wjp_l 5y4l_l 5q85_t 5vr9_t 66th_l 68ed_l 658l_t 63np_t 69z9_t 5zph_t 60hx_t 622t_t 6k91_t 6l1h_ [...]
             <str name="hl.fl">*</str>
             <bool name="hl.requireFieldMatch">true</bool>
             <bool name="lowercaseOperators">true</bool>
diff --git a/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java b/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java
index 0580f7f..d55ada7 100755
--- a/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java
+++ b/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java
@@ -47,6 +47,8 @@ import org.apache.atlas.model.instance.AtlasEntityHeader;
 import org.apache.atlas.model.instance.AtlasObjectId;
 import org.apache.atlas.model.instance.EntityMutationResponse;
 import org.apache.atlas.model.metrics.AtlasMetrics;
+import org.apache.atlas.model.metrics.AtlasMetricsMapToChart;
+import org.apache.atlas.model.metrics.AtlasMetricsStat;
 import org.apache.atlas.model.patches.AtlasPatch.AtlasPatches;
 import org.apache.atlas.model.tasks.AtlasTask;
 import org.apache.atlas.repository.audit.AtlasAuditService;
@@ -78,6 +80,9 @@ import org.apache.commons.lang.RandomStringUtils;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -125,6 +130,7 @@ import static org.apache.atlas.web.filters.AtlasCSRFPreventionFilter.CSRF_TOKEN;
 @Path("admin")
 @Singleton
 @Service
+@EnableScheduling
 public class AdminResource {
     private static final Logger LOG = LoggerFactory.getLogger(AdminResource.class);
     private static final Logger PERF_LOG = AtlasPerfTracer.getPerfLogger("AdminResource");
@@ -145,6 +151,9 @@ public class AdminResource {
     private static final String OPERATION_STATUS               = "operationStatus";
     private static final List TIMEZONE_LIST                    = Arrays.asList(TimeZone.getAvailableIDs());
 
+    private static final String METRICS_PERSIST_INTERVAL         = "atlas.metrics.persist.schedule";
+    private static final String METRICS_PERSIST_INTERVAL_DEFAULT = "0 0 0/1 * * *";     // 1 hour interval
+
     @Context
     private HttpServletRequest httpServletRequest;
 
@@ -409,8 +418,156 @@ public class AdminResource {
         return metrics;
     }
 
-    private void releaseExportImportLock() {
-        importExportOperationLock.unlock();
+    /** Auto-scheduling API for both creating a Metrics entity and saving it to the database at in preset time interval,
+     *  and sweeping through entities that are outside of the valid ttl hours.
+     *  @throws AtlasBaseException when the MetricsStat entity has already existed.
+     */
+    @Scheduled(cron="#{getCronExpression}")
+    public void scheduleSaveAndDeleteMetrics() throws AtlasBaseException {
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> AdminResource.scheduleSaveAndDeleteMetrics()");
+        }
+
+        // auto persist
+        saveMetrics();
+
+        // auto purge
+        metricsService.purgeMetricsStats();
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== AdminResource.scheduleSaveAndDeleteMetrics()");
+        }
+    }
+
+    /**
+     * Bulk retrieval API for getting all MetricsStats, with mininfo flag return metrics with specific details, or with minimal information.
+     * @return all MetricsStats in Atlas.
+     * @throws AtlasBaseException when there is no MetricsStats entity in the database.
+     */
+    @GET
+    @Path("metricsstats")
+    @Produces(Servlets.JSON_MEDIA_TYPE)
+    public List<AtlasMetricsStat> getAllMetrics(@QueryParam("mininfo") @DefaultValue("true") Boolean minInfo) throws AtlasBaseException {
+        AtlasPerfTracer perf = null;
+
+        try {
+            if (AtlasPerfTracer.isPerfTraceEnabled(PERF_LOG)) {
+                perf = AtlasPerfTracer.getPerfTracer(PERF_LOG, "AdminResource.getAllMetrics()");
+            }
+
+            return metricsService.getAllMetricsStats(minInfo);
+        } finally {
+            AtlasPerfTracer.log(perf);
+        }
+    }
+
+    /**
+     * Retrieval API for retrieving the MetricsStat with a specific collectionTime.
+     * @return the MetricsStat with the specific collectionTime.
+     * @throws AtlasBaseException when the MetricsStat entity with this specific collectionTime cannot be found.
+     */
+    @GET
+    @Path("metricsstat/{collectionTime}")
+    @Produces(Servlets.JSON_MEDIA_TYPE)
+    public AtlasMetricsStat getMetricsByCollectionTime(@PathParam("collectionTime") String collectionTime) throws AtlasBaseException {
+        Servlets.validateQueryParamLength("collectionTime", collectionTime);
+
+        AtlasPerfTracer perf = null;
+
+        try {
+            if (AtlasPerfTracer.isPerfTraceEnabled(PERF_LOG)) {
+                perf = AtlasPerfTracer
+                        .getPerfTracer(PERF_LOG,
+                                "AdminResource.getMetricsByCollectionTime(collectionTime=" + collectionTime + ")");
+            }
+
+            return metricsService.getMetricsStatByCollectionTime(collectionTime);
+        } finally {
+            AtlasPerfTracer.log(perf);
+        }
+    }
+
+    /** Retrieval API for retrieving persisted MetricsStats with collectionTime within range of startTime and endTime.
+     * @param startTime start timestamp of the time range.
+     * @param endTime   end timestamp of the time range.
+     * @param typeNames a list of typeNames with their counting information, as well as their metrics' minimal information.
+     * @return          persisted Metrics with its collectionTime within time range, in the form of minimal information.
+     * @throws AtlasBaseException when the input of startTime and endTime is null or invalid.
+     */
+    @GET
+    @Path("metricsstats/range")
+    @Produces(Servlets.JSON_MEDIA_TYPE)
+    public List<AtlasMetricsStat> getMetricsInTimeRange(@QueryParam("startTime") String startTime,
+                                                        @QueryParam("endTime")   String endTime,
+                                                        @QueryParam("typeName")  List<String> typeNames) throws AtlasBaseException {
+        if (StringUtils.isBlank(startTime) || StringUtils.isBlank(endTime)) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, "startTime or endTime is null/empty.");
+        }
+
+        Servlets.validateQueryParamLength("startTime", startTime);
+        Servlets.validateQueryParamLength("endTime", endTime);
+        for (String typeName : typeNames) {
+            Servlets.validateQueryParamLength("typeName", typeName);
+        }
+
+        AtlasPerfTracer perf = null;
+        try {
+            if (AtlasPerfTracer.isPerfTraceEnabled(PERF_LOG)) {
+                perf = AtlasPerfTracer.getPerfTracer(PERF_LOG,
+                        "AdminResource.getMetricsInTimeRange(startTime=" + startTime + ", " +
+                                "endTime="   + endTime   + ", " +
+                                "listOfTypeNames="  + String.join(", ", typeNames)  + ")" );
+            }
+
+            return metricsService.getMetricsInRangeByTypeNames(Long.parseLong(startTime), Long.parseLong(endTime), typeNames);
+        } finally {
+            AtlasPerfTracer.log(perf);
+        }
+    }
+
+    /** Retrieval API for retrieving & formatting MetricsStats (within valid range) to render stacked area chart. The process contains:
+     * 1. retrieve persisted MetricsStats with collectionTime within range of startTime and endTime by one typeName
+     * 2. map the returned MetricsStats to the required format for rendering stacked area chart
+     * Currently, one typeName corresponds to one chart. The API can take multiple typeNames. The returned JSON file can be used to render multiple charts.
+     * @param startTime start timestamp of the time range.
+     * @param endTime   end timestamp of the time range.
+     * @param typeNames a list of typeNames with their counting information, as well as their metrics' minimal information.
+     * @return          formatted metrics to render one or multiple stacked area charts.
+     * @throws AtlasBaseException when the input of startTime and endTime is null or invalid.
+     */
+    @GET
+    @Path("metricsstats/charts")
+    @Produces(Servlets.JSON_MEDIA_TYPE)
+    public Map<String, List<AtlasMetricsMapToChart>> getMetricsForChartByTypeNames(
+            @QueryParam("startTime")  String startTime,
+            @QueryParam("endTime")    String endTime,
+            @QueryParam("typeName")   List<String> typeNames) throws AtlasBaseException {
+
+        if (StringUtils.isBlank(startTime) || StringUtils.isBlank(endTime)) {
+            throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST, "startTime or endTime is null/empty.");
+        }
+
+        Servlets.validateQueryParamLength("startTime", startTime);
+        Servlets.validateQueryParamLength("endTime", endTime);
+        for (String typeName : typeNames) {
+            Servlets.validateQueryParamLength("typeName", typeName);
+        }
+
+        AtlasPerfTracer perf = null;
+        try {
+            if (AtlasPerfTracer.isPerfTraceEnabled(PERF_LOG)) {
+                perf = AtlasPerfTracer.getPerfTracer(PERF_LOG,
+                        "AdminResource.getMetricsForChartByTypeNames(" +
+                                "startTime="        + startTime + ", " +
+                                "endTime="          + endTime   + ", " +
+                                "listOfTypeNames="  + String.join(", ", typeNames)  + ")" );
+            }
+
+            return metricsService.getMetricsForChartByTypeNames(Long.parseLong(startTime), Long.parseLong(endTime), typeNames);
+        } finally {
+            AtlasPerfTracer.log(perf);
+        }
     }
 
     @POST
@@ -867,4 +1024,39 @@ public class AdminResource {
 
         auditService.add(auditOperation, params, AtlasJson.toJson(entityCountByType), resultCount);
     }
+
+    private void releaseExportImportLock() {
+        importExportOperationLock.unlock();
+    }
+
+    /** Get customized time interval to persist metrics in CM, or use default persist hour (1hr interval).
+     * There are 6 fields. Default 1 hr interval: 0 0 0/1 * * *
+     */
+    @Bean
+    private String getCronExpression() {
+        if (atlasProperties != null) {
+            return atlasProperties.getString(METRICS_PERSIST_INTERVAL, METRICS_PERSIST_INTERVAL_DEFAULT);
+        } else {
+            return METRICS_PERSIST_INTERVAL_DEFAULT;
+        }
+    }
+
+    /** Save an AtlasMetrics as AtlasMetricsStat to db.
+     * @throws AtlasBaseException when the AtlasMetricsStat is null or when the AtlasMetricsStat already exists in db.
+     */
+    private void saveMetrics() throws AtlasBaseException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> AdminResource.saveMetrics()");
+        }
+
+        AtlasMetrics metrics = metricsService.getMetrics();
+
+        AtlasMetricsStat metricsStat = new AtlasMetricsStat(metrics);
+        metricsService.saveMetricsStat(metricsStat);
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== AdminResource.saveMetrics()");
+        }
+    }
+
 }