diff --git a/static/css/quay.css b/static/css/quay.css
index cb2578b52..c12083d94 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -557,11 +557,13 @@ p.editable:hover i {
 .repo-image-view .changes-container .change-side-controls {
   float: right;
   clear: both;
+  margin-top: 10px;
+  margin-bottom: 10px;
 }
 
 .repo-image-view .changes-container .filter-input {
   display: inline-block;
-  width: 200px;
+  width: 400px;
 }
 
 .repo-image-view .changes-container .result-count {
@@ -903,6 +905,7 @@ p.editable:hover i {
   margin-bottom: 12px;
 }
 
+
 #image-history-container {
   overflow: hidden;
   min-height: 400px;
@@ -959,8 +962,40 @@ p.editable:hover i {
   cursor: pointer;
 }
 
-#image-history-container .tags .tag.current {
-  
+#changes-tree-container {
+  overflow: hidden;
+}
+
+#changes-tree-container .node rect {
+  cursor: pointer;
+  fill: #fff;
+ fill-opacity: 1;
+  stroke: #fff;
+  stroke-width: 1.5px;
+}
+
+#changes-tree-container .node .change-icon {
+  font-size: 14px;
+}
+
+#changes-tree-container .node text {
+  font: 12px sans-serif;
+  pointer-events: none;
+}
+
+#changes-tree-container .node.added text {
+  fill: rgb(32, 163, 32);
+}
+
+#changes-tree-container .node.removed text {
+  text-decoration: line-through;
+  fill: rgb(209, 73, 73);
+}
+
+#changes-tree-container path.link {
+  fill: none;
+  stroke: #9ecae1;
+  stroke-width: 1.5px;
 }
 
 /* Overrides for the markdown editor. */
diff --git a/static/js/controllers.js b/static/js/controllers.js
index 251699988..316c4f346 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -834,6 +834,15 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
     $scope.search['$'] = filter;
     document.getElementById('change-filter').value = filter;
   };
+  
+  $scope.initializeTree = function() {
+    if ($scope.tree) { return; }
+    
+    $scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges);
+    setTimeout(function() {
+      $scope.tree.draw('changes-tree-container');
+    }, 10);
+  };
 
   // Fetch the image.
   var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid);
diff --git a/static/js/graphing.js b/static/js/graphing.js
index 4794acbd0..615d2523f 100644
--- a/static/js/graphing.js
+++ b/static/js/graphing.js
@@ -713,4 +713,350 @@ ImageHistoryTree.prototype.toggle_ = function(d) {
     d.children = d._children;
     d._children = null;
   }
+};
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock)
+ */
+function ImageFileChangeTree(image, changes) {
+    /**
+     * The parent image.
+     */
+    this.image_ = image;
+
+    /**
+     * The changes being drawn.
+     */
+    this.changes_ = changes;
+
+    /**
+     * Counter for creating unique IDs.
+     */
+    this.idCounter_ = 0;
+   
+    /**
+     * Map from file path to associated tree node.
+     */
+    this.nodeMap_ = {};
+}
+
+
+/**
+ * Calculates the dimensions of the tree.
+ */
+ImageFileChangeTree.prototype.calculateDimensions_ = function(container) {
+    var cw = document.getElementById(container).clientWidth;
+    var barWidth = cw * 0.8;
+
+    var barHeight = 20;
+    var ch = (this.changes_.length * barHeight) + 40;
+
+    var margin = { top: 40, right: 20, bottom: 20, left: 40 };
+    var m = [margin.top, margin.right, margin.bottom, margin.left];
+    var w = cw - m[1] - m[3];
+    var h = ch - m[0] - m[2];
+
+    return {
+        'w': w,
+        'h': h,
+        'm': m,
+        'cw': cw,
+        'ch': ch,
+        'bw': barWidth,
+        'bh': barHeight
+    };
+};
+
+
+/**
+ * Draws the tree.
+ */
+ImageFileChangeTree.prototype.draw = function(container) {
+  this.container_ = container;  
+    
+  var dimensions = this.calculateDimensions_(container);
+  
+  var w = dimensions.w;
+  var h = dimensions.h;
+  var m = dimensions.m;
+
+  this.barWidth_ = dimensions.bw;
+  this.barHeight_ = dimensions.bh;
+
+  var tree = d3.layout.tree()
+    .size([h, 100]);
+
+  var diagonal = d3.svg.diagonal()
+    .projection(function(d) { return [d.y, d.x]; });
+
+  var rootSvg = d3.select("#" + container).append("svg:svg")
+    .attr("width", w)
+    .attr("height", h);
+
+  var vis = rootSvg
+        .append("svg:g")
+        .attr("transform", "translate(" + m[3] + "," + m[0] + ")");
+
+  this.rootSvg_ = rootSvg;
+  this.tree_ = tree;
+  this.diagonal_ = diagonal;
+  this.vis_ = vis;
+
+  this.populateAndDraw_();
+};
+
+
+/**
+ * Populates the tree and then draws it.
+ */
+ImageFileChangeTree.prototype.populateAndDraw_ = function() {
+  // For each change, generate all the node(s) needed for the folders, as well
+  // as the final node (if any) for the file.
+  for (var i = 0; i < this.changes_.length; ++i) {
+    var filepath = this.changes_[i].file;
+    var node = this.buildNodes_(filepath);
+    node.kind = this.changes_[i].kind;
+  }
+
+  // Sort the children of each node so that folders are on top.
+  var sortByName = function (a, b) {
+    if (a.name > b.name) {
+      return 1;
+    }
+    if (a.name < b.name) {
+      return -1;
+    }
+    return 0;
+  };
+
+  var sortFunction = function(a, b) {
+    var hasA = a._children.length > 0;
+    var hasB = b._children.length > 0;
+    if (hasA == hasB) {
+      // Sort alphabetically.
+      return sortByName(a, b);
+    }
+    if (hasA) { return -1; }
+    return 1;
+  };
+
+  for (var path in this.nodeMap_) {
+    if (!this.nodeMap_.hasOwnProperty(path) || !this.nodeMap_[path]._children) {
+      continue;
+    }
+
+    this.nodeMap_[path]._children.sort(sortFunction);
+  }
+
+  this.root_ = this.nodeMap_[''];
+  this.root_.x0 = 0;
+  this.root_.y0 = 0;
+  this.toggle_(this.root_);
+  this.update_(this.root_);
+};
+
+
+/**
+ * Builds all the nodes in the tree.
+ */
+ImageFileChangeTree.prototype.buildNodes_ = function(path) {
+  var parts = path.split('/');
+  for (var i = 0; i < parts.length; ++i) {
+    var currentPath = parts.slice(0, i + 1).join('/');
+    if (!this.nodeMap_[currentPath]) {
+      this.nodeMap_[currentPath] = { 'name': parts[i] || '/', 'path': currentPath, '_children': [] };
+
+      if (currentPath.length > 0) {
+        var parentPath = parts.slice(0, i).join('/');
+        this.nodeMap_[parentPath]._children.push(this.nodeMap_[currentPath]);
+      }
+    }
+  }
+  return this.nodeMap_[path];
+};
+
+
+/**
+ * Calculates the count of visible nodes.
+ */
+ImageFileChangeTree.prototype.getVisibleCount_ = function(node) {
+  if (node.children) {
+    var count = 1;
+    for (var  i = 0; i < node.children.length; ++i) {
+      count += this.getVisibleCount_(node.children[i]);
+    }
+    return count;
+  }
+  return 1;
+};
+
+
+/**
+ * Calculates the height for the container.
+ */
+ImageFileChangeTree.prototype.getContainerHeight_ = function() {
+  var dimensions = this.calculateDimensions_(this.container_);
+  var barHeight = this.barHeight_;
+  var height = (this.getVisibleCount_(this.root_) * (barHeight + 2)); 
+  return height + dimensions.m[0] + dimensions.m[2];
+};
+
+
+/**
+ * Updates the tree starting at the given source node.
+ */
+ImageFileChangeTree.prototype.update_ = function(source) {
+  var that = this;
+  var tree = this.tree_;
+  var vis = this.vis_;
+  var svg = this.rootSvg_;
+  var diagonal = this.diagonal_;
+  var barWidth = this.barWidth_;
+  var barHeight = this.barHeight_;
+
+  var duration = 400;
+
+  var color = function(d) {
+      if (d.kind) {
+        return '';
+      }
+      return d._children ? "#E9E9E9" : "#c6dbef";
+  };
+
+  // Update the height of the container and the SVG.
+  document.getElementById(this.container_).style.height = this.getContainerHeight_() + 'px';
+  svg.attr('height', this.getContainerHeight_());
+
+  // Compute the flattened node list.
+  var nodes = tree.nodes(this.root_);
+  
+  // Compute the "layout".
+  nodes.forEach(function(n, i) {
+    n.x = i * barHeight;
+  });
+  
+  // Update the nodes...
+  var node = vis.selectAll("g.node")
+      .data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });
+  
+  var nodeEnter = node.enter().append("svg:g")
+      .attr("class", function(d) {
+          return "node " + (d.kind ? d.kind : 'folder');
+      })
+      .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
+      .style("opacity", 1e-6);
+
+  // Enter any new nodes at the parent's previous position.
+  nodeEnter.append("svg:rect")
+      .attr("y", -barHeight / 2)
+      .attr("height", barHeight)
+      .attr("width", barWidth)
+      .style("fill", color)
+      .on("click", function(d) { that.toggle_(d); that.update_(source); });
+ 
+  nodeEnter.append("svg:text")
+      .attr("dy", 3.5)
+      .attr("dx", 5.5 + 18)
+      .text(function(d) { return d.name; });
+
+  var body = nodeEnter.append('svg:foreignObject')
+      .attr("x", function(d) { return d.kind ? barWidth - 18 : 0; })
+      .attr("y", -10)
+      .attr("width", 18)
+      .attr("height", barHeight)
+      .append('xhtml:body');
+
+  body.append('div')
+      .attr('class', 'node-icon');
+
+  // Transition nodes to their new position.
+  nodeEnter.transition()
+      .duration(duration)
+      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
+      .style("opacity", 1);
+ 
+  node.transition()
+      .duration(duration)
+       // TODO: reenable for full animation
+       //.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
+      .style("opacity", 1)
+    .select("rect")
+      .style("fill", color);
+
+  // TODO: remove if full animation.
+  node.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
+  
+  node.select('.node-icon')
+      .html(function(d) {
+            if (!d.kind) {
+                var folder = d._children ? 'icon-folder-close' : 'icon-folder-open';
+                return '<i class="' + folder + '"></i>';
+            }
+
+            var icon = {
+                'added': 'plus-sign-alt',
+                'removed': 'minus-sign-alt',
+                'changed': 'edit-sign'
+            };
+
+            return '<i class="change-icon icon-' + icon[d.kind]  + '"></i>';
+        });
+  
+  // Transition exiting nodes to the parent's new position.
+  node.exit().transition()
+      .duration(duration)
+     // TODO: reenable for full animation
+     // .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
+      .style("opacity", 1e-6)
+      .remove();
+  
+  // Update the links...
+  var link = vis.selectAll("path.link")
+      .data(tree.links(nodes), function(d) { return d.target.id; });
+  
+  // Enter any new links at the parent's previous position.
+  link.enter().insert("svg:path", "g")
+      .attr("class", "link")
+      .attr("d", function(d) {
+        var o = {x: source.x0, y: source.y0};
+        return diagonal({source: o, target: o});
+      })
+    .transition()
+      .duration(duration)
+      .attr("d", diagonal);
+  
+  // Transition links to their new position.
+  link.transition()
+      .duration(duration)
+      .attr("d", diagonal);
+  
+  // Transition exiting nodes to the parent's new position.
+  link.exit().transition()
+      .duration(duration)
+      .attr("d", function(d) {
+        var o = {x: source.x, y: source.y};
+        return diagonal({source: o, target: o});
+      })
+      .remove();
+  
+  // Stash the old positions for transition.
+  nodes.forEach(function(d) {
+    d.x0 = d.x;
+    d.y0 = d.y;
+  });
+};
+
+/**
+ * Toggles children of a node.
+ */
+ImageFileChangeTree.prototype.toggle_ = function(d) {
+  if (d.children) {
+    d._children = d.children;
+    d.children = null;
+  } else {
+    d.children = d._children;
+    d._children = null;
+  }
 };
\ No newline at end of file
diff --git a/static/partials/image-view.html b/static/partials/image-view.html
index 333e6c08b..6c02cf9a1 100644
--- a/static/partials/image-view.html
+++ b/static/partials/image-view.html
@@ -19,10 +19,13 @@
     </h3>
    </div>
 
+  <!-- Comment -->
+  <blockquote ng-show="image.comment" ng-bind-html-unsafe="getMarkedDown(image.comment)"></blockquote>
+
   <!-- Information -->
-	<dl class="dl-normal">
-		<dt>Full Image ID</dt>
-		<dd>
+  <dl class="dl-normal">
+    <dt>Full Image ID</dt>
+    <dd>
       <div>
         <div class="id-container">
           <div class="input-group">
@@ -42,32 +45,51 @@
     <dd am-time-ago="parseDate(image.created)"></dd>
   </dl>
 
-  <!-- Comment -->
-  <blockquote ng-show="image.comment" ng-bind-html-unsafe="getMarkedDown(image.comment)"></blockquote>
-
-  <!-- Changes -->
-  <div class="changes-container full-changes-container" ng-show="combinedChanges.length > 0">
+  <!-- Changes tabs -->
+  <div ng-show="combinedChanges.length > 0">
     <b>File Changes:</b>
-    <div class="change-side-controls">      
-      <div class="result-count">
-        Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
-      </div>
-      <div class="filter-input">        
-        <input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
-      </div>
-    </div> 
-    <div class="changes-list well well-sm">
-      <div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
-        No matching changes
-      </div>
-		  <div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
-		    <i ng-class="{'added': 'icon-plus-sign-alt', 'removed': 'icon-minus-sign-alt', 'changed': 'icon-edit-sign'}[change.kind]"></i>
-		    <span title="{{change.file}}">
-          <span style="color: #888;">
-            <span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
-        </span>
-		  </div>
+    <br>
+    <br>
+    <ul class="nav nav-tabs">
+      <li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
+      <li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
+    </ul>
+  </div>
+
+  <!-- Changes tab content -->
+  <div class="tab-content" ng-show="combinedChanges.length > 0">
+    <!-- Filterable view -->
+    <div class="tab-pane active" id="filterable">
+      <div class="changes-container full-changes-container">
+        <div class="change-side-controls">      
+          <div class="result-count">
+            Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
+          </div>
+          <div class="filter-input">        
+            <input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
+          </div>
+        </div> 
+        <div style="height: 28px;"></div>
+        <div class="changes-list well well-sm">
+          <div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
+            No matching changes
+          </div>
+	  <div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
+	    <i ng-class="{'added': 'icon-plus-sign-alt', 'removed': 'icon-minus-sign-alt', 'changed': 'icon-edit-sign'}[change.kind]"></i>
+	    <span title="{{change.file}}">
+              <span style="color: #888;">
+                <span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
+            </span>
 	  </div>
+	</div>
+      </div>
+    </div>
+
+    <!-- Tree view -->
+    <div class="tab-pane" id="tree">
+      <div id="changes-tree-container" class="changes-container"></div>
+    </div>
   </div>
   
+  
 </div>