Skip to content

Instantly share code, notes, and snippets.

@mforando
Last active September 26, 2017 02:59
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 mforando/a7981dc9727bfc06d16498cbbb00258d to your computer and use it in GitHub Desktop.
Save mforando/a7981dc9727bfc06d16498cbbb00258d to your computer and use it in GitHub Desktop.
Bezier Curve 101
license: mit
<!doctype html>
<head>
<meta charset="utf-8">
<title>Dynamic Annotations in a Visualization Stepper</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<style>
body {
font-family: Franklin Gothic Book;
font-size: 0.8em;
}
h1 {
font-family: Franklin Gothic Medium;
font-size: 42px;
margin:0;
color:black;
}
h2.subtitle {
font-family: Franklin Gothic Book;
font-size: .8em;
padding: 0px;
opacity:.7
color: #666;
color:black;
}
a.step-link {
font-family:Franklin Gothic Medium;
text-decoration: none;
z-index: 20;
display:inline-block;
overflow: hidden;
border: .5px solid gray;
font-family:Verdana;
font-size:10.5px;
text-decoration: none;
border: 1px solid gray;
padding: 2px 5px 2px 5px;
color: black;
opacity:.6;
background-color: rgb(240,240,240);
}
a.step-link:last-child {
-webkit-border-radius: 3px;
-moz-border-radius: 3;
border-radius: 0px 3px 3px 0px;
}
a.step-link:first-child {
-webkit-border-radius: 3px;
-moz-border-radius: 3;
border-radius: 3px 0px 0px 3px;
}
a.step-link:hover, a.active {
opacity:1;
background-color: rgb(230,230,230);
}
#container{
margin: auto;
width: 900px;
}
#vis-container {
position: relative;
width: 900px;
height: 710px;
margin-top: 20px;
}
#annotation-steps {
position: absolute;
z-index: 40;
}
#vis-nav {
}
#vis-canvas {
position: absolute;
width: 900px;
height: 710px;
z-index: 10;
overflow: hidden;
background-color: none;
}
#tableauBezier1{
position: absolute;
width: 900px;
height: 710px;
z-index: 10;
overflow: hidden;
background-color: none;
}
#vis-bezier {
position: absolute;
width: 900px;
height: 710px;
z-index: 1000;
overflow: hidden;
background-color: none;
}
.annotation-step {
background-color: green;
position: absolute;
display: none;
z-index:1000;
}
.annotation {
position: absolute;
}
#step1-left-annotation {
left: 540px;
top: 0px;
width: 200px;
text-align:center
}
#step2-low-annotation {
left: 520px;
top: 40px;
width: 300px;
}
#step3-mid-annotation {
left: 480px;
top: 265px;
width: 300px;
text-align: center;
}
.domain {
stroke: rgb(210,210,210);
}
.curve, .line {
stroke: black;
fill: none;
stroke-width: 1px;
stroke-opacity: .6;
}
.curve {
stroke: red;
stroke-width: 3px;
}
.control {
fill: #ccc;
stroke: #000;
stroke-width: .5px;
cursor: move;
}
.control.drag, .control:hover {
fill: #fe0;
}
.t, .controltext {
font-size: .6em;
}
svg {
display: inline-block;
}
</style>
<body>
<div id="container">
<h1>Albany Tableau Users Group: Bezier Curve 101</h1>
<p>
Bezier curves are commonly used in graphic design to draw smooth curves. By creating additional data points within Tableau for each 'real' data point, math can be used to implement the bezier curve using data to define the starting point, ending point, and control points defining the curve. The following sequence of examples shows how a single curve is implemented, how it can be used to visualize political terms, and how it can be tweaked to show different shaped arcs like golf drives.<div id="vis-nav">
<a href="#" id="step0" class="step-link active">Bezier Curve Animation</a><a href="#" id="step1" class="step-link">Tableau Implementation</a><a href="#" id="step2" class="step-link">Inverting the Curve</a><a href="#" id="step3" class="step-link">Supreme Court Tenure Data</a><a href="#" id="step4" class="step-link">Bezier Curve Re-creation</a><a href="#" id="step5" class="step-link">Modifying the Control Points</a>
</div>
<div id="vis-container">
<div id="vis-canvas"></div>
<div id="vis-bezier"></div>
<div id="tableauBezier1"></div>
</div>
</div>
<script src="https://public.tableau.com/javascripts/api/tableau-2.min.js"></script>
<script src="https://public.tableau.com/javascripts/api/tableau-2.2.1.min.js"></script>
<script>
var laststep = "step0";
function tableauViz(url,htmlElement) {
var placeholderDiv = document.getElementById("tableauBezier1");
var options = {
width: placeholderDiv.offsetWidth,
height: placeholderDiv.offsetHeight,
hideTabs: true,
hideToolbar: true,
onFirstInteractive: function () {
workbook = viz.getWorkbook();
activeSheet = workbook.getActiveSheet();
}
};
viz = new tableau.Viz(placeholderDiv, url, options);
}
tableauViz("https://public.tableau.com/views/TableauUsersGroupBezierCurves/BezierExample")
sources = d3.selectAll("#container").append("div").append("p");
var w = 900,
h = 650,
t = .5,
delta = .01,
padding = 10,
bezier = {},
n = 4,
line = d3.line().x(function(d){return d.x;}).y(function(d){return d.y;}),
orders = [4];
var points = [
{x: 0, y: 346},
{x: 160, y: 169},
{x: 84,y: 42}
];
var svgBezier = d3.select("#vis-bezier")
.selectAll("svg")
.data(orders)
.enter()
.append("svg")
.attr("width", w + 2 * padding)
.attr("height", h + 2 * padding)
svgBezier.append("rect").attr("width",w).attr("height",h).attr("fill","white")
var vis = svgBezier.append("g")
.attr("transform", "translate(" + padding + "," + padding + ")")
update();
vis.selectAll("circle.control")
.data(function(d) { return points.slice(0, d) })
.enter().append("circle")
.attr("class", "control")
.attr("r", 7)
.attr("cx", x)
.attr("cy", y)
.call(d3.drag()
.on("start", function(d) {
this.__origin__ = [d.x, d.y];
d3.select(this).classed("drag", true);
})
.on("drag", function(d) {
d.x = Math.min(w, Math.max(0, this.__origin__[0] += d3.event.dx));
d.y = Math.min(h, Math.max(0, this.__origin__[1] += d3.event.dy));
bezier = {};
update();
vis.selectAll("circle.control")
.attr("cx", x)
.attr("cy", y);
})
.on("end", function() {
delete this.__origin__;
d3.select(this).classed("drag", false);
}));
vis.append("text")
.attr("class", "t")
.attr("x", w / 2)
.attr("y", h)
.attr("text-anchor", "middle");
vis.selectAll("text.controltext")
.data(function(d) { return points.slice(0, d); })
.enter().append("text")
.attr("class", "controltext")
.attr("dx", "10px")
.attr("dy", ".4em")
.text(function(d, i) { return "P" + i });
var last = 0;
d3.timer(function(elapsed) {
t = (t + (elapsed - last) / 5000) % 1;
last = elapsed;
update();
});
function update() {
var interpolation = vis.selectAll("g")
.data(function(d) { return getLevels(d, t); });
interpolation.enter().append("g");
var circle = interpolation.selectAll("circle")
.data(Object);
circle.enter().append("circle")
.attr("r", 4);
circle
.attr("cx", x)
.attr("cy", y);
var path = interpolation.selectAll("path")
.data(function(d) { return [d]; });
path.enter().append("path")
.attr("class", "line")
.attr("d", line);
path.attr("d", line);
var curve = vis.selectAll("path.curve")
.data(getCurve);
curve.enter().append("path")
.attr("class", "curve");
console.log(curve);
curve.attr("d", line);
vis.selectAll("text.controltext")
.attr("x", x)
.attr("y", y);
vis.selectAll("text.t")
.text("t=" + t.toFixed(2));
}
function interpolate(d, p) {
if (arguments.length < 2) p = t;
var r = [];
for (var i=1; i<d.length; i++) {
var d0 = d[i-1], d1 = d[i];
r.push({x: d0.x + (d1.x - d0.x) * p, y: d0.y + (d1.y - d0.y) * p});
}
return r;
}
function getLevels(d, t_) {
if (arguments.length < 2) t_ = t;
var x = [points.slice(0, d)];
for (var i=1; i<d; i++) {
x.push(interpolate(x[x.length-1], t_));
}
return x;
}
function getCurve(d) {
var curve = bezier[d];
if (!curve) {
curve = bezier[d] = [];
for (var t_=0; t_<=1; t_+=delta) {
var x = getLevels(d, t_);
curve.push(x[x.length-1][0]);
}
}
return [curve.slice(0, t / delta + 1)];
}
function x(d) {return d.x; }
function y(d) {return d.y; }
function switchStep(newStep)
{
d3.selectAll(".step-link").classed("active", false);
d3.select("#" + newStep).classed("active", true);
if (newStep=="step0"){
svgBezier.attr("opacity",1);
;}
if (newStep=="step1"){
svgBezier.attr("opacity",0);
viz.getWorkbook().changeParameterValueAsync('Y2', 1);
if (laststep!="step2"){
d3.selectAll("#tableauBezier1").remove();
d3.selectAll("#vis-container").append("div").attr("id","tableauBezier1");
tableauViz("https://public.tableau.com/views/TableauUsersGroupArcPlotData/BezierExample")
;}
else {
;}
}
if (newStep=="step2"){
if (laststep!="step1") {
//Until I find a more efficient method, delete embedded Viz and add back with new URL.
d3.selectAll("#tableauBezier1").remove();
d3.selectAll("#vis-container").append("div").attr("id","tableauBezier1");
tableauViz("https://public.tableau.com/views/TableauUsersGroupArcPlotData/BezierExample")
;}
viz.getWorkbook().changeParameterValueAsync('Y2', -1);
}
if (newStep=="step3"){
//Until I find a more efficient method, delete embedded Viz and add back with new URL.
d3.selectAll("#tableauBezier1").remove();
d3.selectAll("#vis-container").append("div").attr("id","tableauBezier1");
tableauViz("https://public.tableau.com/views/TableauUsersGroupArcPlotData/BezierDataset")
}
if (newStep=="step4"){
//Until I find a more efficient method, delete embedded Viz and add back with new URL.
d3.selectAll("#tableauBezier1").remove();
d3.selectAll("#vis-container").append("div").attr("id","tableauBezier1");
tableauViz("https://public.tableau.com/views/TableauUsersGroupArcChartBezierRe-creation/ArcChartRe-creation")
}
if (newStep=="step5"){
//Until I find a more efficient method, delete embedded Viz and add back with new URL.
d3.selectAll("#tableauBezier1").remove();
d3.selectAll("#vis-container").append("div").attr("id","tableauBezier1");
tableauViz("https://public.tableau.com/views/TableauUsersGroupGolfDriveViz/GolfDrivesViz")
}
laststep = newStep;
}
function switchAnnotation(newStep)
{
d3.selectAll(".annotation-step")
.style("display", "none")
.style("opacity", 0.0);
d3.select("#" + newStep + "-annotation")
.style("display", "block")
.transition().delay(300).duration(2000)
.style("opacity", 1);
}
d3.selectAll("a.step-link").on("click", function(d) {
var clickedStep = d3.select(this).attr("id");
switchStep(clickedStep);
switchAnnotation(clickedStep);
return false;
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment