var r = 960 , w = 960 , h = 960 // 500 , u = top.location.pathname , vis = d3.select('#chart').append('svg:svg') .attr('width', r) .attr('height', r) .attr('class', 'bubble') , defs = vis.append('svg:defs') , style = document.createElement('style') , head = document.head || document.getElementsByTagName('head')[0] , css = '' , pref = 'file:' === location.protocol ? '' : 'http://bl.ocks.org/d/1306472/' , prel = new Image // preload the images , surl = prel.src = pref + 'sprites.png' , char_radius = 33 , char_img_sz = 80 , sprite_w = 1025 , sprite_h = 208 , _slice = Array.prototype.slice , C, I, D // character, item and dungeon visualizations respectively , colours = { "LouieCharmeCaillouTielleElanNagiGriffArma": "#BBB" , "LouieCaillouElanGriff": "lightblue" , "CharmeTielleNagiArma": "pink" } , data, item_urls , items, characters, dungeons, locations, categories, recipes, sprites ; d3.select(self.frameElement).style('height', (80 + w + 128) +'px'); load([ pref + 'recettear-data.json' , pref + 'recettear-images.json'], d3.json, loaded); function loaded(files) { data = files[0]; item_urls = files[1]; // url : array of item indices with that image items = data.items; characters = data.characters; dungeons = data.dungeons; locations = data.locations; categories = data.categories; recipes = data.recipes; sprites = data.sprites; characters.forEach(function(c) { colours[c.name] = c.color; }); init(); } function init() { var items_pane = d3.select('.items') , items_node = items_pane.node() , max_height , defs = vis.append('svg:defs') , i = 0 ; for (var url in item_urls) item_urls[url].forEach(function(idx) { var item = items[idx], id; item.value = sum_stats(item); item.img_id = id = 'i' + i++; item.is_item = 1; defs.append('svg:image') .attr('id', id) .attr('width', 1) .attr('height', 1) .attr('xlink:href', url); }); // sort characters by sex, to make some item usage distribution more intuitive characters.sort(function by_sex(a, b) { var A = Number(a.id.slice(2)), B = Number(b.id.slice(2)); return (((A & 1) << 3) + A) - (((B & 1) << 3) + B); }); dungeons.forEach(function (d) { var dx = (d.id - 1) * -128 + 4; css += '.du'+ d.id +' { background-position: '+ dx +'px 0px; }\n'; d.is_dungeon = true; }); style.innerHTML = css; head.appendChild(style); // for character selection (and showing users of items) C = d3.select('ul.characters') .selectAll('li.character').data(characters) .enter().append('li') // "anyone" is in the document already .attr('class', 'character') .append('a') .text(_name) .attr('id', _name) // permalinks .attr('class', _id) // (styling) .attr('title', _name) .attr('onclick', 'by_character(event)') .attr('target', '_top') .attr('href', function(c) { return u +'#'+ c.name; }); d3.select('#Anyone').attr('href', function(c) { return u +'#Anyone'; }); bubbles(); I = d3.select('#chart').selectAll('.item'); /* // the main items pane I = items_pane.selectAll('.item').data(items).enter().append('a') .attr('class', 'item') .attr('href', wiki_url) .attr('id', _item_id) .attr('title', _name); // add item image icon I.append('img').attr('width', 32).attr('height', 32).attr('src', _image); // make items findable via Ctrl/Cmd-F (centering titles below) I.append('label') .text(_name) .attr('for', _item_id) .style('margin-left', function() { return - (this.offsetWidth >> 1) +'px'; }); */ // the dungeon selector D = d3.select('ul.dungeons') .selectAll('li.dungeon').data(dungeons) .enter().append('li') .attr('class', function(d) { return 'dungeon du'+ d.id; }); // make chested items findable by dungeon (and set dungeon expectations) D.append('a') .text(_name) .attr('id', _name) .attr('title', _name) .attr('onclick', 'by_dungeon(event)') .attr('target', '_top') .attr('href', function(d) { return u +'#'+ d.name; }); max_height = window.innerHeight - y_pos(items_node) - 8; // 64 = item badge height, 120 = dungeon icon height items_node.style.height = (max_height - max_height % 64 - 120) + 'px'; document.body.addEventListener('mousemove', show_item, false); document.body.addEventListener('DOMFocusIn', show_item, false); by_character((top.location.hash || '').slice(1)); } function bubbles() { var format = d3.format(",d") , which = items.filter(pluck('value')).sort(sort_by_value) ; var bubble = d3.layout.pack().sort(null).size([r, r]); var node = vis.selectAll('g.node') .data(bubble.nodes({ children: which })) .enter().append('svg:g') .attr('class', 'node item') .attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; }); //node.append('svg:title') // .text(function(d) { return d.name; }); node.append('svg:circle') .attr('class', 'item') .attr('r', function(d) { return d.r; }) .style('fill', function(d) { var chars = d.chars && d.chars.join(''); return chars ? colours[chars] || '#EEE' : 'white'; }); node.append('svg:use') .attr('class', 'item') .attr('xlink:href', function(d) { return '#'+ d.img_id; }) .attr('transform', function(d) { var dx = Math.sqrt(d.r * d.r / 2) , sz = 'scale('+ (2 * dx) +')'; return 'translate(-'+ dx +',-'+ dx +') '+ sz; }) .append('svg:title') .text(function(d) { return d.name +': '+ d.value +' total stats'; }); //node.append('svg:text') // .attr('text-anchor', 'middle') // .attr('dy', '.3em') // .text(function(d) { return d.className.substring(0, d.r / 3); }); } function show_item(e) { function is_user(ch) { if (!item || !item.chars) return false; return -1 !== item.chars.indexOf(ch.name); } var at = e.target, item = at.__data__; if (item && !item.is_item) item = false; C.classed('user', is_user); // if we're hovering the dungeon panel, don't touch it if (!item || item.is_dungeon) return; D.classed('chest', function(d) { return item && is_in_dungeon(d.id, item); }); } function y_pos(node) { var pn = node.offsetParent || 0; return node.offsetTop + (pn && y_pos(pn)); } function _id(c) { return c.id; } function _name(c) { return c.name; } function _image(i) { return i.image; } function pluck(n) { return function(x) { return x && x[n]; }; } function array(a, n) { return _slice.call(a, n||0); } function partial(fn) { var args = array(arguments, 1); return function() { return fn.apply(this, args.concat(array(arguments))); }; } function _item_id(i) { return 'i'+ i.id; } function wiki_url(item) { var name = ( { "Assassin Blade": "Assassin's Blade" })[item.name] || item.name; return 'http://recettear.wikia.com/wiki/' + name.replace(/ /g, '_'); } function sum_stats(i) { return i.atk + i.def + i.mag + i.mdef; } function by_dungeon(e) { var name = 'object' === typeof e ? e.target.id : e , x = window.pageXOffset , y = window.pageYOffset , id, d, i, min, max; for (i = 0; d = dungeons[i]; i++) if (d.name === name) { id = d.id; break; } if (!id) return; min = 10 * id; max = 10 + min; by_character('Anyone'); // reset character view D.classed('selected', 0).classed('chest', 0); d3.select(document.getElementById(name).parentNode).classed('selected', 1); I.transition().duration(250) .style('opacity', function(item) { return item.is_item ? is_in_dungeon(id, item) ? 1 : 0.25 : 1; }); } function is_in_dungeon(no, item) { function exists(min_level) { return min <= min_level && min_level < max; } var min = no * 10, max = 10 + min; return item && (item.where.chest || []).filter(exists).length; } function by_character(e) { var name = 'object' === typeof e ? e.target.id : e , any = 'Anyone' === name , x = window.pageXOffset , y = window.pageYOffset , id, c, i; for (i = 0; c = characters[i]; i++) if (c.name === name) { id = c.id; break; } if (!id && !any) return; //console.info(name); D.classed('chest', 0).classed('selected', 0); d3.select('.characters li.selected').classed('selected', 0); d3.select(document.getElementById(name).parentNode).classed('selected', 1); I.transition() .duration(250) .style('opacity', function(d, i) { if (d && d.is_item && !any && -1 === (d.chars || []).indexOf(name)) { return 0.25; } return 1; }) ; } function sort_by_value(a, b) { return a.value - b.value; } function load(urls, loader, cb) { function fetch(url, n) { function loaded(data) { all[n] = data; if (!--left) cb(all); } loader(url, loaded); } var all = [], left = urls.length; all.urls = urls; urls.forEach(fetch); }