-
-
Save trebor/59ea1df983227bc5beb4d641f1c13117 to your computer and use it in GitHub Desktop.
Face Chartlet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html > | |
<head> | |
<title>Faces</title> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<div class="chart"></div> | |
</body> | |
<script src="https://d3js.org/d3.v4.min.js" type="text/javascript"></script> | |
<script src="https://rawgit.com/twitter/d3kit/v2.0.0/dist/d3kit.js" type="text/javascript"></script> | |
<script src="main.js" type="text/javascript"></script> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var MAX_FACE_SIZE = 100; | |
var FACE_SIZE_SCALE = d3.scaleLinear().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.scaleLinear(); | |
var yScale = d3.scaleLinear(); | |
// 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.scaleLinear().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) + ')'; | |
}) | |
.on('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', function(d) {charlet.getDispatcher().call('clickLeftEye', this, d);}); | |
// 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', function(d) {charlet.getDispatcher().call('clickRightEye', this, d);}); | |
// 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)); | |
}) | |
.on('end', done); | |
} | |
function exit(selection, done) { | |
selection.select('.scale') | |
.transition('scale-down') | |
.attr('transform', 'scale(0)') | |
.on('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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%; | |
} | |
.chart { | |
text-align: center; | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment