Pivot table visualization inspired by zoomable partition layout. It is implemented as Bootsrap modal that you can open by clicking at the button above.
Last active
January 6, 2017 04:28
-
-
Save pkowalicki/57d1deb5003baa826d9e21ec4683b058 to your computer and use it in GitHub Desktop.
Pivot table visualization with zoomable partition layout in d3.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
border: no | |
height: 900 | |
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> | |
<link href="http://bootswatch.com/spacelab/bootstrap.min.css" rel="stylesheet"/> | |
<script src="http://code.jquery.com/jquery-1.11.1.min.js" type="text/javascript"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> | |
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> | |
<style type="text/css"> | |
.chart { | |
display: block; | |
margin: auto; | |
margin-top: 60px; | |
font-size: 11px; | |
} | |
rect { | |
stroke: #eee; | |
fill: #aaa; | |
fill-opacity: .8; | |
} | |
rect.parent { | |
cursor: pointer; | |
fill: steelblue; | |
} | |
text { | |
pointer-events: none; | |
} | |
#analyticsModal .modal | |
{ | |
width: 98%; | |
} | |
#analyticsModal .modal-dialog | |
{ | |
width: 98%; | |
} | |
#analyticsModal .modal-body | |
{ | |
overflow-y: auto; | |
} | |
.custom-bullet li { | |
display: block; | |
} | |
.custom-bullet li:before | |
{ | |
/*Using a Bootstrap glyphicon as the bullet point*/ | |
content: "\e080"; | |
font-family: 'Glyphicons Halflings'; | |
font-size: 9px; | |
float: left; | |
margin-top: 4px; | |
margin-left: -17px; | |
color: #CCCCCC; | |
} | |
.row.extra-bottom-padding { | |
margin-bottom: 20px; | |
} | |
</style> | |
</head> | |
<body role="document"> | |
<!-- ######## Core chart HTML content ########### --> | |
<div class="modal fade collapse" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header" style="text-align: center"> | |
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | |
<h3 class="modal-title" id="analytics-modal-label">Chart title</h3> | |
</div> | |
<div class="modal-body"> | |
<div class="container-fluid" id="modal-body-main-content" style="width:100%; height:100%;"> | |
<div class="row" id="config-row"> | |
<!-- content of the config row is generated dynamically based on the parameters --> | |
</div> <!-- end of config row --> | |
<div class="alert alert-danger alert-dismissible" id="analytics-modal-no-data-alert" role="alert"> | |
<button type="button" class="close" data-hide="alert" aria-label="Close"><span aria-hidden="true">×</span></button> | |
<h4 class="text-center"><strong>Error!</strong> Problem with data source. Check data URL and content.</h4> | |
</div> | |
</div> <!-- end of modal body container for configuration --> | |
</div> <!-- end of modal body --> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> | |
</div> | |
</div> <!-- end of modal content --> | |
</div> <!-- end of modal dialog --> | |
</div> <!-- end of modal --> | |
<div class="hide analytics-dropdown-pattern"> | |
<div class="input-group pull-left"> | |
<div class="input-group-btn"> | |
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">text<span class="caret"></span></button> | |
<ul class="dropdown-menu" role="menu"> | |
</ul> | |
</div> | |
<span class="label"></span> | |
</div> | |
</div> | |
<div class="hide analytics-show-btn-pattern"> | |
<div> | |
<button type="button" id="show-vis-button" class="btn btn-primary pull-left" disabled="disabled">Show</button> | |
</div> | |
</div> | |
<div class="hide analytics-text-pattern"> | |
<h4 class="pull-right"> Choose Levels: </h4> | |
</div> | |
<!-- ######## End of core chart HTML content ########### --> | |
<!-- ######## Explanation (readme) content ############# --> | |
<div class="container-fluid"> | |
<div class="row extra-bottom-padding"> | |
<div class="col-sm-12"> | |
<p class="lead text-left"> Pivot table visualization using Zoomable Partition Layout in <a href="https://d3js.org/">d3.js</a> </p> | |
<p class="text-left"> Visualization is inspired by | |
<a href="http://mbostock.github.io/d3/talk/20111018/partition.html">the implementation</a> of partition layout.</p> | |
<p class="text-left">You can visualize hypothetical data containing sales orders for clothes products. Summarizing aggregation is performed on the number of ordered units for the hierarchy of three dimensions: | |
</p> | |
<ul class="custom-bullet"> | |
<li>the sales person placing an order,</li> | |
<li>the name of the ordered product,</li> | |
<li>the state of an order (open, closed, archived).</li> | |
</ul> | |
<p class="text-left">You can change the hierarchy of dimensions at any time which will trigger visual transition to new data view. Click any field on the chart to ascend and descend. | |
</p> | |
<p class="text-left"><a href="product_orders.json">Data source</a> is in flat structure that can be easily retrieved with the likes of 'group by' statement.</p> | |
</div> | |
</div> | |
<div class="row extra-bottom-padding"> | |
<div class="col-sm-12"> | |
<button type="button" class="btn btn-primary center-block" onclick="testProductOrders()">Click to start</button> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<p class="text-left">If time allows I will | |
migrate it from Bootstrap modal to more generic HTML. In the meantime, if you would like to use it, you need to add | |
core chart HTML content to your DOM, invoke <code>openVisualizationModal(dimensionsTable, dataSourceURLFn, chartTitle)</code> function and prepare the data in <a href="product_orders.json">such</a> flat structure. | |
</p> | |
</div> | |
</div> | |
</div> | |
<!-- ######## End of explanation (readme) content ############# --> | |
<!-- ######## Core visualization code ######################### --> | |
<script type="text/javascript" src="pivot_table_vis.js"></script> | |
<script type="text/javascript"> | |
// opens visualization chart | |
// arguments: d - table with dimensions | |
// function(dims) - function returning data source URL | |
// string - title of the chart | |
function testProductOrders(){ | |
var d = ["state", "owner", "product"]; | |
if (!openVisualizationModal(d, function(dims){ | |
return "product_orders.json"; | |
}, "Product Orders")) | |
alert("ERROR. Unable to open visualization."); | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var dimensions = []; // definition of dimensions | |
var dimensionsValues = []; // current choice for each dimension | |
var previousDimValues = []; // previous choice of dimensions values (for rollback in case of error) | |
var MAX_DIM = 6; // maximum number of dimensions | |
// titles, labels etc. - strings used in the config controls | |
var EMPTY_DIM_LABEL_TEXT = "EMPTY"; | |
var DIM_BUTTON_TEXT = "LEVEL"; | |
var DEFAULT_TITLE = "Analytics"; | |
var MODAL_TITLE = ""; | |
var dataSourceURLFn; | |
//helper property definition for getting the d3 selection size (useful for debugging) | |
d3.selection.prototype.size = function() { | |
var n = 0; | |
this.each(function() { ++n; }); | |
return n; | |
}; | |
// visualization dimensions | |
var w = 800, | |
h = 500; | |
// scaling functions for x and y coordinates that maps the visualized data domain (values) to visualization viewport size | |
var x = d3.scale.linear().range([0, w]), | |
y = d3.scale.linear().range([0, h]); | |
var kx, ky; | |
//visualization viewport | |
var vis = null; | |
var g = null; // global variable containing the group of visualized 'svg:g' elements | |
var treeRoot = null; // root for visualized data | |
var isInChangeDimension = false; // flag indicating that the visualization is in fading out stage | |
//----- visualization config controls functions comes below ----------------------- | |
//inits drop-downlists with the content based on the passed dimensions | |
function initConfigDropdowns(){ | |
for (i=1, len=dimensions.length; i<=len; i++) | |
for (j=0; j<len; j++) | |
$("div.input-group[dimensionId='" + i + "'] ul") | |
.append("<li dimensionName='" + dimensions[j] + "'>" + | |
"<a href='#'>" + dimensions[j] + "</a></li>"); | |
} | |
// on-click handlers for the config drop-down lists | |
function registerDimensionsOnClick(){ | |
$("li[dimensionName] > a").click(function(){ | |
var dimensionId = parseInt($(this).parents(".input-group").attr("dimensionId")); | |
var dimensionValue = $(this).text(); | |
// disable this dimension value in this dropdown | |
$(this).parent().addClass("disabled"); // disable this dimension value in this dropdown | |
// remove link block on old value if any | |
if (dimensionsValues[dimensionId-1] != ""){ | |
$("div.input-group[dimensionId='" + dimensionId + "'] li[dimensionName='" + dimensionsValues[dimensionId-1] + "']") | |
.removeClass("disabled"); | |
} | |
// update dimension label | |
$("div.input-group[dimensionId='" + dimensionId + "'] > span") | |
.removeClass("label-danger") | |
.addClass("label-success") | |
.text(dimensionValue); | |
var index = dimensionsValues.indexOf(dimensionValue); | |
if (index != -1){ | |
var updatingDimensionId = index + 1; | |
$("div.input-group[dimensionId='" + updatingDimensionId + "'] li[dimensionName='" + dimensionValue + "']") | |
.removeClass("disabled"); | |
$("div.input-group[dimensionId='" + updatingDimensionId + "'] > span") | |
.removeClass("label-success") | |
.addClass("label-danger") | |
.text(EMPTY_DIM_LABEL_TEXT); | |
dimensionsValues[index] = ""; | |
} | |
dimensionsValues[dimensionId-1] = dimensionValue; | |
if (dimensionsValues.indexOf("") == -1){ | |
$("#show-vis-button").prop('disabled', false); | |
} | |
else | |
$("#show-vis-button").prop('disabled', true); | |
}); | |
} | |
//creates the modal title based on the chosen dimensions | |
function buildTitleString(){ | |
var dimensionsOrder = " - "; | |
for (i=0, len=dimensionsValues.length; i<len; i++){ | |
dimensionsOrder = dimensionsOrder.concat(dimensionsValues[i]); | |
if (i != len-1) dimensionsOrder = dimensionsOrder.concat(", "); | |
} | |
dimensionsOrder = dimensionsOrder.concat("."); | |
$("#analytics-modal-label").text(MODAL_TITLE + dimensionsOrder); | |
} | |
// fires visualization | |
function registerShowOnClick(){ | |
$("#show-vis-button").click(function(){ | |
d3.json(dataSourceURLFn(dimensionsValues)) | |
.on("progress", function(){ | |
$("#analytics-modal-label").text("Loading data ..."); | |
$("#show-vis-button").text("Loading data ..."); | |
$("#show-vis-button").prop('disabled', true); | |
}) | |
.get(function(error, root){ | |
if (typeof root === "undefined"){ | |
rollbackDimensions(previousDimValues); | |
$("#analytics-modal-no-data-alert").show(); | |
return; | |
} | |
previousDimValues = dimensionsValues.slice(0); | |
var data = d3.nest(); | |
function addKey(index) { | |
data.key(function(d) { return d[dimensionsValues[index]]; }) | |
} | |
for(var i=0; i<dimensionsValues.length-1; i++) { | |
addKey(i); | |
} | |
var visObj = {"key": root.name, "values" : data.entries(root.data)}; | |
var statisticsName = root.statistics; | |
if ($("div.chart g").length == 0){ | |
visualize(visObj, statisticsName); | |
} else { | |
fadeOut(); | |
d3.timer(function(){ | |
if ($("div.chart g").length == 0){ | |
visualize(visObj, statisticsName); | |
return true; | |
} | |
}); | |
} | |
}); // end of xhr.get | |
}); // end of on-click | |
} | |
// restores previously chosen dimensions in case of error (values, dropdown links' states as well as labels are rollbacked) | |
function rollbackDimensions(values){ | |
$("#analytics-modal-label").text(MODAL_TITLE); | |
$("#show-vis-button").text("Show"); | |
dimensionsValues = values.slice(0); | |
$("li[dimensionName]").removeClass("disabled"); | |
if (values.indexOf("") == -1) | |
{ | |
$("#show-vis-button").prop('disabled', false); | |
for (i=0, len=values.length; i<len; i++){ | |
$("div.input-group[dimensionId='" + (i+1) + "'] li[dimensionName='" + values[i] + "']") | |
.addClass("disabled"); | |
$("div.input-group[dimensionId='" + (i+1) + "'] > span") | |
.addClass("label-success") | |
.text(values[i]); | |
} | |
} | |
else{ | |
$("div.input-group[dimensionId] > span") | |
.addClass("label-danger") | |
.text(EMPTY_DIM_LABEL_TEXT); | |
$("#show-vis-button").prop('disabled', true); | |
} | |
} | |
// init all the text fields in the modal (label, dimensions buttons' texts and modal title) | |
function initModalTexts(){ | |
$("div.input-group[dimensionId] > span") | |
.addClass("label-danger") | |
.text(EMPTY_DIM_LABEL_TEXT); | |
$("div.input-group-btn button:first-child", $("#analyticsModal")) | |
.each(function(i){ | |
$(this) | |
.html(DIM_BUTTON_TEXT | |
+ " " | |
+ $(this).parents(".input-group").attr("dimensionId") | |
+ " <span class='caret'></span>"); | |
}); | |
$("#analytics-modal-label").html(MODAL_TITLE); | |
} | |
// this function creates dynamically the config row based on the passed dimensions | |
function initConfigRow(){ | |
var sideWidth = MAX_DIM - dimensions.length; | |
$(".analytics-text-pattern") | |
.clone() | |
.removeClass("hide analytics-text-pattern") | |
.addClass("col-sm-" + sideWidth) | |
.appendTo($("#config-row", $("#analyticsModal"))); | |
for (i=0, len=dimensions.length; i<len; i++) | |
$(".analytics-dropdown-pattern") | |
.clone() | |
.removeClass("hide analytics-dropdown-pattern") | |
.addClass("col-sm-2") | |
.children(".input-group") | |
.attr("dimensionId",i+1) | |
.parent() | |
.appendTo($("#config-row", $("#analyticsModal"))); | |
$(".analytics-show-btn-pattern") | |
.clone() | |
.removeClass("hide analytics-show-btn-pattern") | |
.addClass("col-sm-" + sideWidth) | |
.appendTo($("#config-row", $("#analyticsModal"))); | |
} | |
// this function shall be called from the external code to open and init the analytics modal | |
// it expects the table of dimension names as strings e.g. ["dim1", "dim2", "dim3"] | |
// URL of data source and title of the analytics | |
function openVisualizationModal(dims, getDataSourceURL, title){ | |
if(typeof dims === "undefined" || !$.isArray(dims)){ | |
console.log("Wrong arguments passed to openning analytics modal: dimensions table incorrect"); | |
return false; | |
} | |
if(!getDataSourceURL){ | |
console.log("Wrong arguments passed to openning analytics modal: please provide function returning your data URL"); | |
return false; | |
} | |
dataSourceURLFn = getDataSourceURL; | |
dimensions = dims; | |
if (typeof title === "undefined") MODAL_TITLE = DEFAULT_TITLE | |
else MODAL_TITLE = title; | |
$("#analyticsModal").one("show.bs.modal", function() { | |
var height = $(window).height() - 100; | |
$(this).find(".modal-body").css("max-height", height); | |
for(i=0,len=dimensions.length; i<len; i++){ | |
dimensionsValues[i] = ""; | |
previousDimValues[i] = ""; | |
} | |
$("#show-vis-button") | |
.prop('disabled', true) | |
.text("Show"); | |
$("button[data-hide='alert']", $("#analyticsModal")).click(function(){ | |
$("#analytics-modal-no-data-alert").hide(); | |
}); | |
$("#analytics-modal-no-data-alert").hide(); | |
// creates the container and SVG placeholder for visualization | |
vis = d3.select(".modal-body").append("div") | |
.attr("class", "chart") | |
.style("width", w + "px") | |
.style("height", h + "px") | |
.append("svg:svg") | |
.attr("width", w) | |
.attr("height", h); | |
initConfigRow(); | |
initModalTexts(); | |
initConfigDropdowns(); | |
registerDimensionsOnClick(); | |
registerShowOnClick(); | |
}); | |
// register handler for clearing things up after modal is closed | |
$("#analyticsModal").one("hidden.bs.modal", function() { | |
// removes list items for dimensions choice (and all the on-click handlers) | |
$("div.input-group li", $("#analyticsModal")).remove(); | |
// remove chart container where visualization is displayed and all linked event handlers | |
$("div.chart", $("#analyticsModal")).remove(); | |
// remove on-click handler for Show button | |
$("#show-vis-button").off("click"); | |
//remove alert close on-click | |
$("button[data-hide='alert']", $("#analyticsModal")).off("click"); | |
// empty config row | |
$("#config-row > div", $("#analyticsModal")).remove(); | |
dimensions = []; | |
dimensionsValues = []; | |
previousDimValues = []; | |
}); | |
// show the modal | |
$("#analyticsModal").modal('show'); | |
return true; | |
} | |
// ----------------- Visualization code comes below ----------------------------- | |
// helper function to apply callback at the end of all independent transitions scheduled on a group of elements | |
function endall(transition, callback) { | |
var n = 0; | |
transition | |
.each(function() { ++n; }) | |
.each("end", function() { if (!--n) callback.apply(this, arguments); }); | |
} | |
// definition of the function visualizing data attached to the root of the JSON tree object | |
function visualize(root, statisticsName){ | |
isInChangeDimension = false; | |
//d3.parition initialization, setting value and children accessors | |
var partition = d3.layout.partition() | |
.value(function(d) { return d[statisticsName]; }) | |
.children(function(d) { return d.values; }); | |
x = d3.scale.linear().range([0, w]); | |
y = d3.scale.linear().range([0, h]); | |
$("#analytics-modal-label").text("Rendering ..."); | |
$("#show-vis-button").text("Rendering ..."); | |
$("#show-vis-button").prop('disabled', true); | |
treeRoot = root; | |
g = vis.selectAll("g") | |
.data(partition.nodes(root)) | |
.enter().append("svg:g") | |
.attr("transform", function(d) { return "translate(" + x(d.y) + ", 0)"; }) | |
.on("click", click); | |
kx = w / root.dx; | |
ky = h / 1; | |
g.append("svg:rect") | |
.attr("width", root.dy * kx) | |
.attr("height", 0) | |
.attr("class", function(d){ return d.children ? "parent" : "child"; }); | |
g.append("svg:text") | |
.attr("transform", "translate(8,0)") | |
.attr("dy", ".35em") | |
.style("opacity", 0) | |
.text(function(d) { | |
return (d.children ? d.key : d[dimensionsValues[dimensionsValues.length-1]]) + ": " + d.value; | |
}); | |
var t = g.transition() | |
.duration(750) | |
.attr("transform", function(d) { return "translate(" + x(d.y) + "," + y(d.x) + ")"; }) | |
.call(endall, function(){ | |
buildTitleString(); | |
$("#show-vis-button").text("Show"); | |
$("#show-vis-button").prop('disabled', false); | |
}); | |
t.select("rect") | |
.attr("width", root.dy * kx) | |
.attr("height", function(d) { return d.dx * ky; }); | |
t.select("text") | |
.attr("transform", function(d){ | |
return "translate(8," + d.dx * ky / 2 + ")"; | |
}) | |
.attr("dy", ".35em") | |
.style("opacity", function(d) { return d.dx * ky > 12 ? 1 : 0; }); | |
} // end of visualize function | |
// data element on-click handler to zoom in/out the visualization | |
function click(d) { | |
$("#show-vis-button").prop('disabled', true); | |
if (!d.children) | |
d = treeRoot; | |
kx = (d.y ? w - 40 : w) / (1 - d.y); | |
ky = h / d.dx; | |
x.domain([d.y, 1]).range([d.y ? 40 : 0, w]); | |
y.domain([d.x, d.x + d.dx]); | |
var gTransition = g.transition() | |
.duration(1500) | |
.attr("transform", function(d) { return "translate(" + x(d.y) + "," + y(d.x) + ")"; }) | |
.call(endall, function(){ | |
if (!isInChangeDimension && dimensionsValues.indexOf("") == -1) | |
$("#show-vis-button").prop('disabled', false); | |
}); | |
var rectTransition = gTransition.select("rect") | |
.attr("width", d.dy * kx) | |
.attr("height", function(d) { return d.dx * ky; }); | |
var textTransition = gTransition.select("text") | |
.attr("transform", function(d){ | |
return "translate(8," + d.dx * ky / 2 + ")"; | |
}) | |
.style("opacity", function(d) { return d.dx * ky > 12 ? 1 : 0; }); | |
if (isInChangeDimension){ | |
gTransition.transition() | |
.duration(1500) | |
.attr("transform", function(d) { return "translate(" + x(d.y) + ", 0)"; }) | |
.remove() | |
.call(endall, function(){ | |
if (dimensionsValues.indexOf("") == -1) | |
$("#show-vis-button").prop('disabled', false); | |
}); | |
rectTransition.transition() | |
.duration(1500) | |
.attr("height", 0); | |
textTransition.transition() | |
.duration(1500) | |
.attr("transform", "translate(8,0)") | |
.style("opacity",0); | |
} | |
} | |
// fading out the current visualization | |
function fadeOut() { | |
$("#analytics-modal-label").text("Rendering ..."); | |
$("#show-vis-button").text("Rendering ..."); | |
$("#show-vis-button").prop('disabled', true); | |
isInChangeDimension = true; | |
if (!(kx == w && ky == h)) click(treeRoot); | |
else { | |
var t = g.transition() | |
.duration(2000) | |
.attr("transform", function(d) { return "translate(" + x(d.y) + ", 0)"; }) | |
.remove() | |
.call(endall, function(){ | |
$("#show-vis-button").prop('disabled', false); | |
}); | |
t.select("rect") | |
.attr("height", 0); | |
t.select("text") | |
.attr("transform", "translate(8,0)") | |
.style("opacity",0); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "product orders total", | |
"statistics" : "no_products", | |
"data": [ | |
{ | |
"no_products": 100, | |
"state": "open", | |
"owner": "Jacob", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 70, | |
"state": "open", | |
"owner": "Jacob", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 140, | |
"state": "open", | |
"owner": "Noah", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 100, | |
"state": "open", | |
"owner": "Noah", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 50, | |
"state": "open", | |
"owner": "Noah", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 70, | |
"state": "open", | |
"owner": "William", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 60, | |
"state": "open", | |
"owner": "William", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 50, | |
"state": "open", | |
"owner": "William", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 190, | |
"state": "open", | |
"owner": "Emily", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 140, | |
"state": "open", | |
"owner": "Emily", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 100, | |
"state": "open", | |
"owner": "Emily", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 70, | |
"state": "open", | |
"owner": "Emma", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 140, | |
"state": "open", | |
"owner": "Emma", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 180, | |
"state": "closed", | |
"owner": "Jacob", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 140, | |
"state": "closed", | |
"owner": "Jacob", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 80, | |
"state": "closed", | |
"owner": "Jacob", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 190, | |
"state": "closed", | |
"owner": "William", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 110, | |
"state": "closed", | |
"owner": "William", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 40, | |
"state": "closed", | |
"owner": "William", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 200, | |
"state": "closed", | |
"owner": "Emily", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 100, | |
"state": "closed", | |
"owner": "Emily", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 130, | |
"state": "closed", | |
"owner": "Noah", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 100, | |
"state": "closed", | |
"owner": "Noah", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 190, | |
"state": "closed", | |
"owner": "Emma", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 140, | |
"state": "closed", | |
"owner": "Emma", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 30, | |
"state": "closed", | |
"owner": "Emma", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 20, | |
"state": "closed", | |
"owner": "Emma", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 200, | |
"state": "archived", | |
"owner": "Jacob", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 140, | |
"state": "archived", | |
"owner": "Jacob", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 80, | |
"state": "archived", | |
"owner": "Jacob", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 20, | |
"state": "closed", | |
"owner": "Jacob", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 110, | |
"state": "archived", | |
"owner": "William", | |
"product": "belt" | |
}, | |
{ | |
"no_products": 80, | |
"state": "archived", | |
"owner": "William", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 20, | |
"state": "archived", | |
"owner": "William", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 100, | |
"state": "archived", | |
"owner": "Emily", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 90, | |
"state": "closed", | |
"owner": "Emily", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 130, | |
"state": "archived", | |
"owner": "Noah", | |
"product": "jacket" | |
}, | |
{ | |
"no_products": 100, | |
"state": "archived", | |
"owner": "Noah", | |
"product": "cap" | |
}, | |
{ | |
"no_products": 40, | |
"state": "archived", | |
"owner": "Noah", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 130, | |
"state": "archived", | |
"owner": "Emma", | |
"product": "trousers" | |
}, | |
{ | |
"no_products": 120, | |
"state": "archived", | |
"owner": "Emma", | |
"product": "t-shirt" | |
}, | |
{ | |
"no_products": 100, | |
"state": "archived", | |
"owner": "Emma", | |
"product": "cap" | |
}, { | |
"no_products": 60, | |
"state": "archived", | |
"owner": "Emma", | |
"product": "belt" | |
}, { | |
"no_products": 10, | |
"state": "archived", | |
"owner": "Emma", | |
"product": "jacket" | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment