Skip to content

Instantly share code, notes, and snippets.

@PatMartin
Last active February 25, 2017 03:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PatMartin/69b321040ff265f8888f6f8e04a4f268 to your computer and use it in GitHub Desktop.
Save PatMartin/69b321040ff265f8888f6f8e04a4f268 to your computer and use it in GitHub Desktop.
Chart Explorer
license: mit

Chart Explorer

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.

  • Drag 2 or more series onto the series drag target at the very top.
  • Drag charts onto the chart drag target.

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.

Credits

I'd like to give a shout out to a couple of authors who have made some excellent drag and drop plugins.

  • Nicolas Bevacqua - Author of Dragula. If you are in need of an intuitive drag and drop implementation, this one is awesome!
  • David S Morse - Author/maintainer of gridster.js which is a super slick drag and drop grid implementation. Dragula and Gridster work together to make this demo possible.

References

[
{
"image": "C3AreaChart.png",
"configuration": {
"name": "C3AreaChart",
"components": [
{
"name": "C3AreaChart",
"class": "dex.charts.c3.AreaChart",
"config": {}
}
]
}
},
{
"image": "C3LineChart.png",
"configuration": {
"name": "C3LineChart",
"components": [
{
"name": "C3LineChart",
"class": "dex.charts.c3.LineChart",
"config": {}
}
]
}
},
{
"image": "C3BarChart.png",
"configuration": {
"name": "C3BarChart",
"components": [
{
"name": "C3BarChart",
"class": "dex.charts.c3.BarChart",
"config": {}
}
]
}
},
{
"image": "Chord.png",
"configuration": {
"name": "Chord",
"components": [
{
"name": "Chord",
"class": "dex.charts.d3.Chord",
"config": {
"margin": {
"top": 0,
"bottom": 0,
"left": 0,
"right": 0
}
}
}
]
}
},
{
"image": "ParallelCoordinates.png",
"configuration": {
"name": "ParallelCoordinates",
"components": [
{
"name": "ParallelCoordinates",
"class": "dex.charts.d3.ParallelCoordinates",
"config": {
"margin.top": 50,
"margin.bottom": 25,
"margin.left": 50,
"margin.right": 50
}
}
]
}
},
{
"image": "ClusteredForce.png",
"configuration": {
"name": "ClusteredForce",
"components": [
{
"name": "ClusteredForce",
"class": "dex.charts.d3.ClusteredForce",
"config": {}
}
]
}
},
{
"image": "Treemap.png",
"configuration": {
"name": "Treemap",
"components": [
{
"name": "Treemap",
"class": "dex.charts.d3.Treemap",
"config": {}
}
]
}
},
{
"image": "Dendrogram.png",
"configuration": {
"name": "Dendrogram",
"components": [
{
"name": "Dendrogram",
"class": "dex.charts.d3.Dendrogram",
"config": {}
}
]
}
},
{
"image": "PC2Chord.png",
"configuration": {
"name": "PC2Chord",
"components": [
{
"name": "PC",
"class": "dex.charts.d3.ParallelCoordinates",
"config": {
"class": "PC2Chord",
"margin": {
"top": 50,
"bottom": 10,
"left": 20,
"right": 20
}
}
},
{
"name": "Chord",
"class": "dex.charts.d3.Chord",
"config": {
"class": "PC2Chord",
"margin": {
"top": 0,
"bottom": 0,
"left": 0,
"right": 0
}
}
}
],
"interactions": [
{
"sources": [
"PC"
],
"destinations": [
"Chord"
],
"event": "select",
"action": "setSelected"
}
]
}
}
]
<!DOCTYPE HTML>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment