Skip to content

Instantly share code, notes, and snippets.

@mforando
Created September 27, 2017 03:14
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/62a6d02f2b78dc144ed265f0fe516778 to your computer and use it in GitHub Desktop.
Save mforando/62a6d02f2b78dc144ed265f0fe516778 to your computer and use it in GitHub Desktop.
Tableau Users Group: Intro To Bezier Curves
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;
overflow: hidden;
background-color: none;
}
#tableauBezier1{
position: absolute;
width: 900px;
height: 710px;
overflow: hidden;
background-color: none;
}
#vis-bezier {
position: absolute;
width: 900px;
height: 710px;
overflow: hidden;
z-index: 1000;
background-color: none;
}
.annotation-step {
background-color: green;
position: absolute;
display: none;
z-index:1000;
}
.annotation {
position: absolute;
}
#step0-left-annotation {
left: 10px;
top: 40px;
width: 240px;
text-align:left;
}
#step0-low-annotation {
left: 0px;
top: 425px;
width: 900px;
text-align:center;
}
#step1-left-annotation {
left: 280px;
top: 35px;
width: 240px;
text-align:left;
}
#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;
}
.curve {
stroke-width: 2px;
stroke: red;
stroke-opacity:.8;
}
.t {
font-size: 44px;
}
</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="tableauBezier1"></div>
<div class="annotation-step" id="step0-annotation" style="display:block;">
<div class="annotation" id="step0-left-annotation" >
<p>The animation to the right demonstrates the basic concept behind a quadtratic bezier curve. In general, the bezier curve interpolates between the intitial control points and then continues to interpolate between the resulting points until a single point on the desired curve is determined. The value for t shown in the top left corner ranges from 0 to 1 and represents where in the curve the animation is at. <strong>Click/drag on the control points below to see how it affects the curve that is drawn.</strong></p>
</div>
<div class="annotation" id="step0-low-annotation" >
<p>Concept and original D3 code modified from: <a href="https://www.jasondavies.com/animated-bezier/">https://www.jasondavies.com/animated-bezier/</a></p>
</div>
</div>
<div class="annotation-step" id="step1-annotation">
<div class="annotation" id="step1-left-annotation" >
<p>Using data densification techniques (creating additional data points for each original/'real' data point), a bezier curve can be implemented in Tableau by feeding in one or more Tableau measures in the bezier formula. In some cases, the bezier curve height can be modified, while the width of the arc is modified in others, if not both.</p>
</div>
</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>
BezierAnimation()
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");
function switchStep(newStep)
{
d3.selectAll(".step-link").classed("active", false);
d3.select("#" + newStep).classed("active", true);
if (newStep=="step0"){
BezierAnimation()
;}
else {
d3.selectAll("#vis-bezier").remove();
}
if (newStep=="step1"){
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/TableauUsersGroupBezierCurves/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/TableauUsersGroupBezierCurves/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;
});
function BezierAnimation() {
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 lineGenerator = d3.line();
var points = [
{x: 200, y: 300},
{x: 450, y: 50},
{x: 700,y: 300}
];
var svgBezier = d3.select("#vis-canvas").append("div").attr("id","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 + ")")
var controlCircles = vis.append("g");
var curve = svgBezier.append("g").attr("transform", "translate(" + padding + "," + padding + ")").append("path").attr("class", "curve");
var PathData = [];
vis.selectAll("circle.control")
.data(function(d) {return points.slice(0, d) })
.enter()
.append("circle")
.attr("class", "control")
.attr("z-index",10000)
.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) {
getBezier();
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", 70)
.attr("y", 25)
.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 });
getBezier();
update();
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");
curve.attr("d", lineGenerator(PathData.slice(0,(t/.01+1))));
var circle = interpolation.selectAll("circle").data(Object);
circle.enter().append("circle").attr("r", 3);
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);
//on each iteration, grab the portion of the bezier curve needed to be drawn.
vis.selectAll("text.controltext").attr("x", x).attr("y", y);
vis.selectAll("text.t").text("t=" + t.toFixed(2));
}
//function to recreate an array of points for curve path. Run with drag events to update array.
function getBezier(){
x1 = points[0].x;
x2 = points[1].x;
x3 = points[2].x;
y1 = points[0].y;
y2 = points[1].y;
y3 = points[2].y;
PathData = [];
for (z=0; z<=1.00001; z+=.01) {
BezierX = Math.pow((1-z),2) * x1 + 2*(1-z)*z*x2 + Math.pow(z,2)*x3;
BezierY = Math.pow((1-z),2) * y1 + 2*(1-z)*z*y2 + Math.pow(z,2)*y3;
PathData.push([BezierX,BezierY]);
;}
;}
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 x(d) {return d.x; }
function y(d) {return d.y; }
;}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment