This example demonstrates usage of a new library called d3-component and Redux to create a scatter plot with menus for X, Y, and color, and some animations. It's an experiment to see how things play out when pairing D3 directly with Redux, without React in the middle.
The data shown here comes from the UCI Machine Learning Repository: Auto MPG Data Set.
Built with blockbuilder.org
forked from curran's block: Spinner with d3-component
forked from curran's block: Scatter Plot with Menus X Y Only
forked from curran's block: Scatter Plot with Menus X Y Only Simple Dots
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/d3@4.10.0/build/d3.js"></script>
<script src="https://unpkg.com/d3-component@3.0"></script>
<script src="https://unpkg.com/redux@3/dist/redux.min.js"></script>
<script src="https://unpkg.com/d3-tip@0.7.1"></script>
<link rel="stylesheet" href="https://unpkg.com/bootstrap@4.0.0-alpha.6/dist/css/bootstrap.min.css">
<style>
.point {
fill: currentColor;
}
/* Tooltip styles copied from
https://bl.ocks.org/Caged/6476579 */
.d3-tip {
line-height: 1;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
</style>
</head>
<body>
<script>
// This stateless component renders a static "wheel" made of circles,
// and rotates it depending on the value of props.angle.
var wheel = d3.component("g")
.create(function (selection){
var minRadius = 4,
maxRadius = 10,
numDots = 10,
wheelRadius = 40,
rotation = 0,
rotationIncrement = 3,
radius = d3.scaleLinear()
.domain([0, numDots - 1])
.range([maxRadius, minRadius]),
angle = d3.scaleLinear()
.domain([0, numDots])
.range([0, Math.PI * 2]);
selection
.selectAll("circle").data(d3.range(numDots))
.enter().append("circle")
.attr("cx", function (d){ return Math.sin(angle(d)) * wheelRadius; })
.attr("cy", function (d){ return Math.cos(angle(d)) * wheelRadius; })
.attr("r", radius);
})
.render(function (selection, d){
selection.attr("transform", "rotate(" + d + ")");
});
// This component with a local timer makes the wheel spin.
var spinner = (function (){
var timer = d3.local();
return d3.component("g")
.create(function (selection, d){
timer.set(selection.node(), d3.timer(function (elapsed){
selection.call(wheel, elapsed * d.speed);
}));
})
.render(function (selection, d){
selection.attr("transform", "translate(" + d.x + "," + d.y + ")");
})
.destroy(function(selection, d){
timer.get(selection.node()).stop();
return selection
.attr("fill-opacity", 1)
.transition().duration(3000)
.attr("transform", "translate(" + d.x + "," + d.y + ") scale(10)")
.attr("fill-opacity", 0);
});
}());
var axis = (function (){
var axisLocal = d3.local();
return d3.component("g")
.create(function (selection, d){
axisLocal.set(selection.node(), d3["axis" + d.type]());
selection
.attr("opacity", 0)
.call(axisLocal.get(selection.node())
.scale(d.scale)
.ticks(d.ticks || 10))
.transition("opacity").duration(2000)
.attr("opacity", 0.8);
})
.render(function (selection, d){
selection
.attr("transform", "translate(" + [
d.translateX || 0,
d.translateY || 0
] + ")")
.transition("ticks").duration(3000)
.call(axisLocal.get(selection.node()));
});
}());
// This component displays the visualization.
var scatterPlot = (function (){
var xScale = d3.scaleLinear(),
yScale = d3.scaleLinear();
function render(selection, d){
var x = d.x,
y = d.y,
color = d.color,
margin = d.margin,
innerWidth = d.width - margin.left - margin.right,
innerHeight = d.height - margin.top - margin.bottom;
xScale
.domain(d3.extent(d.data, function (d){ return d[x]; }))
.range([0, innerWidth]);
yScale
.domain(d3.extent(d.data, function (d){ return d[y]; }))
.range([innerHeight, 0]);
selection
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(axis, [
{
type: "Left",
scale: yScale,
translateX: -12
},
{
type: "Bottom",
scale: xScale,
translateY: innerHeight + 12,
ticks: 20
}
])
var circles = selection.selectAll(".point").data(d.data);
circles.exit().remove();
circles
.enter().append("circle")
.attr("class", "point")
.attr("r", 0)
.attr("cx", d.width / 2 - margin.left)
.attr("cy", d.height / 2 - margin.top)
.merge(circles)
.on("mouseover", d.show)
.on("mouseout", d.hide)
.transition()
.duration(2000)
.delay(function (d, i){ return i * 5; })
.attr("r", 5)
.attr("cx", function (d){ return xScale(d[x]); })
.attr("cy", function (d){ return yScale(d[y]); });
}
return d3.component("g")
.render(render);
}());
// Leverage the d3-tip library for tooltips.
var tooltip = (function (){
var tip = d3.tip()
.attr("class", "d3-tip")
.offset([-10, 0]);
return function (svgSelection, state){
// Wish we could use D3 here for DOM manipulation..
tip.html(function(d) {
return [
"<h4>" + d.year + " " + d.name + "</h4>",
"<div><strong>" + state.x + ": </strong>",
"<span>" + d[state.x] + "</span></div>",
"<div><strong>" + state.y + ": </strong>",
"<span>" + d[state.y] + "</span></div>",
"<div><strong>" + state.color + ": </strong>",
"<span>" + d[state.color] + "</span></div>"
].join("");
});
svgSelection.call(tip);
return {
show: tip.show,
hide: tip.hide
};
}
}());
// This component manages an svg element, and
// either displays a spinner or text,
// depending on the value of the `loading` state.
var svg = d3.component("svg")
.render(function (selection, d){
var svgSelection = selection
.attr("width", d.width)
.attr("height", d.height)
.call(spinner, !d.loading ? [] : {
x: d.width / 2,
y: d.height / 2,
speed: 0.2
});
var tipCallbacks = tooltip(svgSelection, d);
svgSelection
.call(scatterPlot, d.loading ? [] : d, tipCallbacks);
});
var label = d3.component("label", "col-sm-2 col-form-label")
.render(function (selection, d){
selection.text(d);
});
var option = d3.component("option")
.render(function (selection, d){
selection.text(d);
});
var select = d3.component("select", "form-control")
.render(function (selection, d){
selection
.call(option, d.columns)
.property("value", d.value)
.on("change", function (){
d.action(this.value);
})
});
var rowComponent = d3.component("div", "row");
var colSm10 = d3.component("div", "col-sm-10");
var menu = d3.component("div", "col-sm-4")
.render(function (selection, d){
var row = rowComponent(selection).call(label, d.label);
colSm10(row).call(select, d);
});
var menus = d3.component("div", "container-fluid")
.create(function (selection){
selection.style("opacity", 0);
})
.render(function (selection, d){
rowComponent(selection).call(menu, [
{
label: "X",
value: d.x,
action: d.setX,
columns: d.numericColumns
},
{
label: "Y",
value: d.y,
action: d.setY,
columns: d.numericColumns
}
], d);
if(!d.loading && selection.style("opacity") === "0"){
selection.transition().duration(2000)
.style("opacity", 1);
}
});
var app = d3.component("div")
.render(function (selection, d){
selection.call(menus, d).call(svg, d);
});
function loadData(actions){
var numericColumns = [
"acceleration",
"cylinders",
"displacement",
"horsepower",
"weight",
"year",
"mpg"
],
ordinalColumns = [
"cylinders",
"origin",
"year"
];
setTimeout(function (){ // Show off the spinner for a few seconds ;)
d3.csv("auto-mpg.csv", type, function (data){
actions.ingestData(data, numericColumns, ordinalColumns)
});
}, 2000);
function type(d){
return numericColumns.reduce(function (d, column){
d[column] = + d[column];
return d;
}, d);
}
}
function reducer (state, action){
var state = state || {
width: 960,
height: 500 - 38,
loading: true,
margin: {top: 12, right: 12, bottom: 40, left: 50},
x: "acceleration",
y: "horsepower",
color: "cylinders"
};
switch (action.type) {
case "INGEST_DATA":
return Object.assign({}, state, {
loading: false,
data: action.data,
numericColumns: action.numericColumns,
ordinalColumns: action.ordinalColumns
});
case "SET_X":
return Object.assign({}, state, { x: action.column });
case "SET_Y":
return Object.assign({}, state, { y: action.column });
case "SET_COLOR":
return Object.assign({}, state, { color: action.column });
default:
return state;
}
}
function actionsFromDispatch(dispatch){
return {
ingestData: function (data, numericColumns, ordinalColumns){
dispatch({
type: "INGEST_DATA",
data: data,
numericColumns: numericColumns,
ordinalColumns: ordinalColumns
});
},
setX: function (column){
dispatch({ type: "SET_X", column: column });
},
setY: function (column){
dispatch({ type: "SET_Y", column: column });
},
setColor: function (column){
dispatch({ type: "SET_COLOR", column: column });
}
};
}
function main(){
var store = Redux.createStore(reducer),
actions = actionsFromDispatch(store.dispatch);
renderApp = function(){
d3.select("body").call(app, store.getState(), actions);
}
renderApp();
store.subscribe(renderApp);
loadData(actions);
}
main();
</script>
</body>
https://unpkg.com/d3@4.10.0/build/d3.js
https://unpkg.com/d3-component@3.0
https://unpkg.com/redux@3/dist/redux.min.js
https://unpkg.com/d3-tip@0.7.1