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; };