This is a simple dashboard which allows the user to drag and drop series and charts onto the palette and visualize the data in a number of different ways.
Series can be reordered via drag and drop. To remove a selected series, simply drag it to somewhere outside of the series drag area.
Charts may also be ordered, reordered, resized, added and deleted.
I am working through a number of minor bugs, but its good enough to demo.
Also noteworthy, charts are plugged in via json specifications and defined in an external json file called charts.json.
I'd like to give a shout out to a couple of authors who have made some excellent drag and drop plugins.
xxxxxxxxxx
<html>
<header>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.theme.min.css">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery.gridster/0.5.6/jquery.gridster.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.css"/>
<style>
#available-series {
float: left;
width: auto;
margin-right: 2%;
padding: 5px;
background-color: rgba(0, 0, 0, 0.2);
transition: opacity 0.4s ease-in-out;
cursor: move;
cursor: grab;
}
#selected-series {
float: left;
width: auto;
height: auto;
min-height: 40px;
min-width: 200px;
margin-right: 2%;
padding: 5px;
background-color: rgba(0, 0, 0, 0.2);
transition: opacity 0.4s ease-in-out;
cursor: move;
cursor: grab;
}
#available-charts {
float: left;
width: auto;
margin-right: 2%;
padding: 5px;
background-color: rgba(0, 0, 0, 0.2);
transition: opacity 0.4s ease-in-out;
cursor: move;
cursor: grab;
overflow: scroll;
}
#chart-container {
height: 100%;
width: 100%;
}
ul {
padding: 1px;
}
li {
list-style: none;
padding: 1px;
margin: 1px;
}
.horizontal li {
display: inline-block;
}
html, body, #main-table, #main-layout-table {
width: 100%;
height: 100%;
}
#widget-table {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
table-layout: fixed !important;
}
#widget-header {
width: 100%;
height: 20px;
}
#widget-header-left {
width: 100%;
height: 20px;
background-color: #3e999f;
}
#widget-header-right {
height: 20px;
}
#widget-content {
width: 100%;
height: 100%;
}
.btn:hover {
opacity: .7 !important;
}
.gridster {
width: 100%;
height: 100%;
}
.gridster .gs-w {
background: white;
cursor: pointer;
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.gridster .player {
-webkit-box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3);
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3);
background: #BBB;
}
.gridster .preview-holder {
border: none !important;
border-radius: 0 !important;
background: red !important;
}
.gridster ul {
background-color: #EFEFEF;
padding-left: 0px;
width: 100% !important;
min-height: 100px !important;
}
.gridster li {
font-size: 1em;
font-weight: bold;
text-align: center;
line-height: 100%;
}
ul {
list-style-type: none;
}
li {
list-style: none;
font-weight: bold;
}
/* Disable the mirror when interacting with the dex charts */
.gu-mirror {
display: none !important;
}
/* Treemap styling */
.TreemapClass text {
pointer-events: none;
}
.TreemapClass .grandparent text {
font-weight: bold;
}
.TreemapClass rect {
fill: none;
stroke: #fff;
}
.TreemapClass rect.parent,
.grandparent rect {
stroke-width: 2px;
}
.TreemapClass .grandparent rect {
fill: steelblue;
}
.grandparent:hover rect {
fill: #ee9700;
}
.TreemapClass .children rect.parent,
.grandparent rect {
cursor: pointer;
}
.TreemapClass .children rect.parent {
fill-opacity: .1;
}
.TreemapClass .children:hover rect.child {
fill-opacity: .8;
}
</style>
</header>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/js/bootstrap.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://dexjs.net/js/dex.js"></script>
<script>d3 = dex.charts.d3.d3v3;</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.gridster/0.5.6/jquery.gridster.min.js"></script>
<table id="main-table">
<tr>
<td valign="top" width="120px">
<ul id="available-series"></ul>
</td>
<td valign="top">
<table id="main-layout-table">
<tr>
<td valign="top" height="50px">
<ul class="horizontal" id="selected-series"></ul>
</td>
</tr>
<tr>
<td width="100%" halign="left">
<div class="gridster">
<ul id="chart-container"></ul>
</div>
</td>
</tr>
</table>
</td>
<td valign="top" width="120px" height="100%">
<ul>
<ul id="available-charts"></ul>
</ul>
</td>
</tr>
</table>
<script>
var csv = {
'header': ['i', 'x', 'y', 'a', 'b', 'c'],
'data': dex.datagen.randomIndexedIntegerMatrix({
rows: 20, columns: 6, min: 0, max: 20
})
};
var colorScheme = d3.scale.category20c();
var chartCsv = {};
var selectedSeries = [];
var charts = [];
var gridster;
var gridsterConfig = {
widget_base_dimensions: [50, 50],
widget_margins: [1, 1],
//helper: 'clone',
animate: false,
draggable: {
enabled: true,
start: function (e, ui) {
dex.console.log("DRAG-START", e, ui);
},
handle: '.drag-handle'
},
resize: {
enabled: true,
min_size: [1, 1],
stop: function (event, ui, $widget) {
dex.console.log("Event", event, ui, $widget);
charts.forEach(function (chartGroup) {
if (chartGroup instanceof Array) {
chartGroup.forEach(function (chart) {
chart.resize();
})
}
else {
chartGroup.resize();
}
});
}
}
};
d3.json("charts.json", function (error, availableCharts) {
var chartLookup = {};
availableCharts.forEach(function (chart) {
chartLookup[chart.configuration.name] = chart.configuration;
});
d3.select("#available-series")
.selectAll("li")
.data(csv.header)
.enter()
.append("li")
.append("button")
.attr("type", "button")
.attr("class", "btn")
.style("width", "100px")
.style("color", "white")
.style("border-color", "white")
.style("background-color", function (d) {
return colorScheme(d);
})
.html(function (d) {
return d;
});
d3.select("#available-charts")
.selectAll("li")
.data(availableCharts)
.enter()
.append("li")
.append("img")
.attr("src", function (d) {
return d.image;
})
.attr("alt", function (d) {
return d.configuration.name;
})
.attr("height", 60)
.attr("width", 100)
.style("border", 0);
// Handle adding series.
var addSeriesDrake = dragula([document.getElementById("available-series"), document.getElementById("selected-series")], {
copy: true,
direction: 'horizontal',
removeOnSpill: true
});
// Handle drop event for adding a series.
addSeriesDrake.on('drop', function (el, target, src, sibling) {
dex.console.log("drop-add-series", el, target, src, sibling);
selectedSeries = $("#selected-series li").map(function () {
return $(this).text();
}).get();
//dex.console.log("SELECTED-SERIES", selectedSeries);
chartCsv = dex.csv.columnSlice(csv, selectedSeries);
//dex.console.log("CHART-CSV", chartCsv);
charts.forEach(function (chartGroup) {
if (chartGroup instanceof Array) {
chartGroup.forEach(function (chart) {
//dex.console.log("Updating chart: " + chart.attr("id"));
//dex.console.log("chartcsv", chartCsv);
chart.attr("csv", chartCsv).update();
});
}
else {
//dex.console.log("Updating chart: " + chartGroup.attr("id"));
//dex.console.log("chartcsv", chartCsv);
chartGroup.attr("csv", chartCsv).update();
}
});
});
// Support for removing a series.
var removeSeriesDrake = dragula([document.getElementById("selected-series")], {
removeOnSpill: true
});
removeSeriesDrake.on('remove', function (el, target, src, sibling) {
selectedSeries = $("#selected-series li").map(function () {
return $(this).text();
}).get();
//dex.console.log("DESELECTED-SERIES", selectedSeries);
chartCsv = dex.csv.columnSlice(csv, selectedSeries);
//dex.console.log("CHART-CSV", chartCsv);
charts.forEach(function (chartGroup) {
if (chartGroup instanceof Array) {
chartGroup.forEach(function (chart) {
chart.attr("csv", chartCsv).update();
})
}
else {
chartGroup.attr("csv", chartCsv).update();
}
});
});
var addChartDrake = dragula([document.getElementById("available-charts"),
document.getElementById("chart-container")], {
copy: true,
removeOnSpill: true,
});
addChartDrake.on('drop', function (el, target, src, sibling) {
// Cancel normal dragula drag and drop, we will handle it.
addChartDrake.cancel(true);
selectedSeries = $("#selected-series li").map(function () {
return $(this).text();
}).get();
if (selectedSeries.length < 2) {
alert("You must select at least 2 series before you may select a chart.");
return;
}
var chartId = el.childNodes[0].alt;
var parentId = chartId + "Parent" + charts.length;
var chartConfig = chartLookup[chartId];
var widget = "<li><table id='widget-table' style='word-break:break-all;'>" +
"<tr id='widget-header'><td id='widget-header-left' class='drag-handle'>|||</td>" +
"<td id='widget-header-right'><button type='button' class='btn gridster-remove' " +
"id='close-chart-button' style='font-size: 100%; " +
"text-align: top; float: right;'>X</button></td></tr>" +
"<tr><td id='widget-content' colspan='2'><div id='" + parentId + "' height='100%'" +
" width='100%'></div></td></tr></table></li>"
gridster.add_widget(widget, 5, 5);
var components = dex.create(chartConfig);
var chartNum = 1;
var parent = d3.select("#" + parentId);
components.forEach(function (chart) {
parent.append("div")
.style("height", "90%")
.style("width", "100%")
.attr("id", parentId + "_" + chartNum);
chart.attr("parent", "#" + parentId + "_" + chartNum);
chart.attr("id", chartId + "Id_" + chartNum);
chart.attr("csv", chartCsv);
chart.render();
chartNum++;
});
charts.push(components);
});
});
$(document).ready(function () {
gridster = $(".gridster ul")
.gridster(gridsterConfig)
.data('gridster');
$(document).on("click", "#close-chart-button", function () {
dex.console.log("CLOSING", $(this));
gridster.remove_widget($(this).closest('li'));
// REM: Timing issue. Update the charts list after giving
// gridster a second to remove the widget.
setTimeout(function () {
charts.forEach(function (chartGroup, i) {
var parentId;
if (chartGroup instanceof Array) {
parentId = chartGroup[0].attr("parent");
if ($(parentId).length == 0) {
chartGroup.forEach(function(chart) { chart.deleteChart(); });
charts.splice(i, 1);
return;
}
}
else {
parentId = chartGroup.attr("parent");
if ($(parentId).length == 0) {
chartGroup.deleteChart();
charts.splice(i, 1);
return;
}
}
});
}, 1000);
});
});
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js
https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js
https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js
https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/js/bootstrap.js
https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js
https://dexjs.net/js/dex.js
https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.js
https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js
https://cdnjs.cloudflare.com/ajax/libs/jquery.gridster/0.5.6/jquery.gridster.min.js