Skip to content

Instantly share code, notes, and snippets.

@treboresque
Last active August 19, 2016 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save treboresque/6cc9d948be0635d88990 to your computer and use it in GitHub Desktop.
Save treboresque/6cc9d948be0635d88990 to your computer and use it in GitHub Desktop.
Face Chartlet
<!DOCTYPE html>
<html >
<head>
<title>Faces</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="chart"></div>
</body>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="https://rawgit.com/twitter/d3kit/v0.1.0/dist/d3kit.min.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
</html>
var MAX_FACE_SIZE = 100;
var FACE_SIZE_SCALE = d3.scale.linear().range([20, MAX_FACE_SIZE]);
var nextId = 0;
var FACE_COUNT = 10;
var DEFAULT_OPTIONS = {
margin: {top: MAX_FACE_SIZE, right: MAX_FACE_SIZE, bottom: MAX_FACE_SIZE, left: MAX_FACE_SIZE}
};
var data = d3.range(FACE_COUNT).map(createFace);
var xScale = d3.scale.linear();
var yScale = d3.scale.linear();
// create a random face
function createFace() {
return {
id: nextId++,
x: Math.random(),
y: Math.random(),
ego: FACE_SIZE_SCALE(Math.random()),
happiness: Math.random(),
};
}
// create and configure the charlet with
// properties needed by charlet
var face = Face()
.property('name', function(d) {return d.id; })
.property('size', function(d) {return d.ego; })
.property('smile', function(d) {return d.happiness; })
// handle internal events from charlet
.on('clickLeftEye', handleLeftPoke)
.on('clickRightEye', handleRightPoke)
// handle event when exit completes on the charlet
.on('exitDone', removeFaceNode);
// sad when left eye poked
function handleLeftPoke(d) {
d3.event.cancelBubble = true;
d.happiness = d3.max([0, d.happiness - 0.2]);
onResize();
}
// happy when right eye poked (go figure)
function handleRightPoke(d) {
d3.event.cancelBubble = true;
d.happiness = d3.min([1, d.happiness + 0.2]);
onResize();
}
// create chart
var chart = new d3Kit.Skeleton('.chart', DEFAULT_OPTIONS)
.autoResize('both')
.on('resize', onResize)
.on('data', onData);
chart.data(data);
chart.resizeToFitContainer();
// remove selected face and add a new one
function handleFaceClick(d, i) {
data.splice(data.indexOf(d), 1);
data.push(createFace());
chart.data(data);
}
// cope with data change
function onData(data) {
if (chart.hasData()) {
var nodes = chart.getRootG().selectAll('g.node')
.data(data, function(d) {return d.id;});
nodes.enter()
.append('g')
.classed('node', true)
.on('click', handleFaceClick)
.call(face.enter);
nodes.exit()
.call(face.exit);
onResize();
}
}
// remove face node, linked ot face.exit
function removeFaceNode(selection) {
selection.remove();
}
// handle resize
function onResize() {
xScale.range([0, chart.getInnerWidth ()]);
yScale.range([0, chart.getInnerHeight()]);
chart.getRootG().selectAll('.node')
.attr('transform', function(d) {
var x = xScale(d.x);
var y = yScale(d.y);
return 'translate(' + x + ',' + y + ')';
})
.call(face.update);
}
// face chartlet
function Face() {
var MOUTH_PATTERN = 'M P0 Q P1 P2';
var MOUTH_DROP = 0.5;
var MOUTH_WIDE = 0.32;
var EMOTIONAL_RANGE = 0.4;
var MOUTH_RANGE = d3.scale.linear().range([
MOUTH_DROP - EMOTIONAL_RANGE,
MOUTH_DROP + EMOTIONAL_RANGE
]);
var events = ['clickLeftEye', 'clickRightEye'];
var charlet = d3Kit.Chartlet(enter, update, exit, events);
function enter(selection, done) {
var scaleGroup = selection
.append('g')
.classed('scale', true)
.attr('transform', function(d) {
return 'scale(0)';
});
scaleGroup
.transition('scale-up')
.attr('transform', function(d) {
return 'scale(' + charlet.getPropertyValue('size', d) + ')';
})
.each('end', done);
// head
scaleGroup
.append('circle')
.classed('head', true)
.attr('fill', 'white')
.attr('stroke', '#888')
.attr('stroke-width', '0.02')
.attr('r', 1);
// forehead tatoo
scaleGroup
.append('text')
.classed('tat', true)
.attr('dy', -0.4)
.attr('text-anchor', 'middle')
.style('font-size', '0.3px')
.text(function(d) {return charlet.getPropertyValue('name', d);});
// left eye
scaleGroup
.append('circle')
.classed('eye', true)
.classed('left', true)
.attr('fill', 'black')
.attr('cx', -0.4)
.attr('cy', -0.1)
.attr('r', 0.2)
.on('click', charlet.getDispatcher().clickLeftEye);
// right eye
scaleGroup
.append('circle')
.classed('eye', true)
.classed('right', true)
.attr('fill', 'black')
.attr('cx', 0.4)
.attr('cy', -0.1)
.attr('r', 0.2)
.on('click', charlet.getDispatcher().clickRightEye);
// right mouth
scaleGroup
.append('path')
.classed('mouth', true)
.attr('stroke', 'black')
.attr('stroke-width', 0.1)
.attr('stroke-linecap', 'round')
.attr('fill', 'none')
.attr('d', function(d) {
return createMouthPath(0.5);
});
}
function update(selection, done) {
// update mouth based on smile value
selection.select('.mouth')
.transition('move-mouth')
.attr('d', function(d) {
return createMouthPath(charlet.getPropertyValue('smile', d));
})
.each('end', done);
}
function exit(selection, done) {
selection.select('.scale')
.transition('scale-down')
.attr('transform', 'scale(0)')
.each('end', done);
}
function createMouthPath(smile) {
return pathPoints(MOUTH_PATTERN, [
{x: -MOUTH_WIDE, y: MOUTH_DROP },
{x: 0 , y: MOUTH_RANGE(smile)},
{x: MOUTH_WIDE , y: MOUTH_DROP },
]);
}
function pathPoints(pattern, points) {
return points.reduce(function(acc, point, i) {
return acc.replace('P' + i, point.x + ' ' + point.y);
}, pattern);
}
return charlet;
};
html {
font-family: 'Gotham';
color: #444;
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
}
body {
background: #ccc;
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
}
.header {
height: 150px;
}
h1 {
margin: 0px;
/* padding-top: 20px; */
text-align: center;
}
.chart {
text-align: center;
position: absolute;
width: 100%;
height: 100%;
/* top: 150px; */
/* left: 100px; */
/* bottom: 100px; */
/* right: 100px; */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment