See the blog post on how and why it was made.
forked from russellgoldenberg's block: Weighted pivot scatter plot v3
| license: mit | |
| height: 500 | |
| border: no |
See the blog post on how and why it was made.
forked from russellgoldenberg's block: Weighted pivot scatter plot v3
| *{box-sizing:border-box}body{color:#333;font-family:Helvetica,Arial,sans-serif}.graphic__hed{text-align:center;color:#333}.chart{max-width:40rem;margin:0 auto;text-align:center}.slider{max-width:20rem;margin:1rem auto;position:relative;padding-top:16.5px}.slider input{display:block;width:100%}.slider:before{content:'Quality';text-transform:uppercase;font-size:11px;letter-spacing:.05em;color:#888;display:inline-block;position:absolute;top:0;left:0}.slider:after{content:'Quantity';text-transform:uppercase;font-size:11px;letter-spacing:.05em;color:#888;display:inline-block;position:absolute;top:0;right:0}.g-axis line,.g-axis path{stroke:#ccc}.g-axis text{fill:#888}.g-axis .axis__label{text-transform:uppercase;font-size:11px;letter-spacing:.05em;fill:#888}.item{fill-opacity:.75} |
| "use strict";function weightData(t){var a=t.x,e=t.y;return dummyData.map(function(t){return _extends({},t,{score:t.x*a+t.y*e})}).sort(function(t,a){return d3.descending(t.score,a.score)}).map(function(t,a){return _extends({},t,{rank:a})}).reverse()}function getHypotenuse(t){var a=t.x,e=t.y,n=a*a,r=e*e;return Math.sqrt(n+r)}function resize(){var t=.8*Math.min(el.node().offsetWidth,window.innerHeight);chart.width(t).height(t),el.call(chart)}function scatterplot(){function t(t,a){return"translate("+t+", "+a+")"}function a(t){var a=t.container,e=t.data,n=a.selectAll("svg").data([e]),r=n.enter().append("svg"),i=r.append("g");i.append("g").attr("class","g-plot");var c=i.append("g").attr("class","g-axis"),s=c.append("g").attr("class","axis axis--x"),o=c.append("g").attr("class","axis axis--y");s.append("text").attr("class","axis__label").attr("text-anchor","start").text("Quantity"),o.append("text").attr("class","axis__label").attr("text-anchor","end").text("Quality")}function e(t){t.container,t.data}function n(t){var a=t.data;v=getHypotenuse({x:p,y:m});var e=p/v*g,n=m/v*x,r=Math.floor(1.5*FONT_SIZE);o.domain([0,MAX_VAL]).range([0,e]),l.domain([0,MAX_VAL]).range([n,0]),d.domain([0,a.length]).range([r,2]),u.domain(a.map(function(t){return t.rank})).range(COLORS)}function r(a){var e=a.container,n=(a.data,e.select("svg"));n.attr("width",h).attr("height",f);var r=n.select("g"),i=l.range()[0],c=g/2,m=x-i,y=Math.acos(p/v),_=90-180*y/Math.PI,M="rotate("+-_+" 0 "+l.range()[0]+")",w=t(1.5*s+c,s+m),A=w+" "+M;r.attr("transform",A);var L=r.select(".g-plot"),O=L.selectAll(".item").data(function(t){return t},function(t){return t.index});O.enter().append("circle").attr("class","item").merge(O).attr("x",0).attr("y",0).attr("r",function(t){return d(t.rank)}).style("fill",function(t){return u(t.rank)}).style("stroke",function(t){return d3.color(u(t.rank)).darker(.7)}).attr("transform",function(a){return t(o(a.x),l(a.y))})}function i(a){var e=a.container,n=(a.data,e.select(".g-axis")),r=d3.axisLeft(l),i=d3.axisBottom(o);r.ticks(Math.max(0,Math.floor(m/10))),i.ticks(Math.max(0,Math.floor(p/10)));var c=n.select(".axis--x"),d=l.range()[0],u=d,h=Math.ceil(s/2);c.attr("transform",t(0,h+u)).call(i);var f=n.select(".axis--y");f.attr("transform",t(-h,0)).call(r),c.select(".axis__label").attr("y",s-1),f.select(".axis__label").attr("x",u).attr("y",s-1).attr("transform","rotate(90)")}function c(t){var c=t.datum();a({container:t,data:c}),e({container:t,data:c}),n({container:t,data:c}),r({container:t,data:c}),i({container:t,data:c})}var s=3*FONT_SIZE,o=d3.scaleLinear(),l=d3.scaleLinear(),d=d3.scaleSqrt(),u=d3.scaleQuantile(),h=0,f=0,g=0,x=0,p=50,m=50,v=0;return c.width=function(){return arguments.length?(h=arguments.length<=0?void 0:arguments[0],g=h-2.5*s,c):h},c.height=function(){return arguments.length?(f=arguments.length<=0?void 0:arguments[0],x=f-2.5*s,c):f},c.weight=function(t){var a=t.x,e=t.y;return p=a,m=e,c},c}function handleInput(){var t=+this.value,a=t,e=100-t,n=weightData({x:a,y:e});chart.weight({x:a,y:e}),el.datum(n),el.call(chart)}function init(){el.datum(weightData({x:50,y:50})),el.call(chart),resize(),window.addEventListener("resize",resize),graphic.select(".slider input").on("input",handleInput)}var _extends=Object.assign||function(t){for(var a=1;a<arguments.length;a++){var e=arguments[a];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])}return t},graphic=d3.select(".graphic--3"),COLORS=["#ff3814","#fe5c34","#fc764f","#f88d69","#f2a385","#e8b8a0","#dbcdbd"],FONT_SIZE=11,MAX_VAL=10,dummyData=d3.range(0,50).map(function(t){return{index:t,x:.5+Math.random()*(MAX_VAL-1),y:.5+Math.random()*(MAX_VAL-1)}}),chart=scatterplot(),el=graphic.select(".chart");init(); | |
| //# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJ3ZWlnaHREYXRhIiwiX3JlZiIsIngiLCJ5IiwiZHVtbXlEYXRhIiwibWFwIiwiZCIsIl9leHRlbmRzIiwic2NvcmUiLCJjaGFydCIsInNjYXR0ZXJwbG90Iiwic29ydCIsImEiLCJiIiwiZDMiLCJkZXNjZW5kaW5nIiwicmFuayIsImkiLCJyZXZlcnNlIiwiZ2V0SHlwb3RlbnVzZSIsIl9yZWYyIiwieTIiLCJNYXRoIiwic3FydCIsInJlc2l6ZSIsIm1pbiIsImVsIiwibm9kZSIsIm9mZnNldFdpZHRoIiwid2luZG93IiwiaW5uZXJIZWlnaHQiLCJ3aWR0aCIsInN6IiwiaGVpZ2h0IiwiY2FsbCIsInNjYWxlWCIsInNjYWxlTGluZWFyIiwiZW50ZXIiLCJfcmVmMyIsImNvbnRhaW5lciIsImRhdGEiLCJzZWxlY3RBbGwiLCJzdmciLCJhcHBlbmQiLCJjaGFydFdpZHRoIiwic3ZnRW50ZXIiLCJnRW50ZXIiLCJ3ZWlnaHRYIiwiYXR0ciIsImh5cG90ZW51c2UiLCJ0cmFuc2xhdGUiLCJheGlzIiwidGV4dCIsImV4aXQiLCJfcmVmNCIsIl9yZWY1Iiwid2VpZ2h0WSIsInJhbmdlWSIsImNoYXJ0SGVpZ2h0IiwiRk9OVF9TSVpFIiwiZG9tYWluIiwiTUFYX1ZBTCIsInJhbmdlIiwicmFuZ2VYIiwic2NhbGVZIiwic2NhbGVSIiwibWF4UiIsInNjYWxlQyIsInVwZGF0ZURvbSIsIl9yZWY2IiwiQ09MT1JTIiwibWF4WSIsIm9mZnNldFgiLCJhbmdsZSIsInJhZCIsIlBJIiwidHJhbnNsYXRpb24iLCJtYXJnaW4iLCJvZmZzZXRZIiwicm90YXRpb24iLCJnIiwidHJhbnNmb3JtIiwicGxvdCIsInNlbGVjdCIsImluZGV4IiwiaXRlbSIsIm1lcmdlIiwic3R5bGUiLCJjb2xvciIsImRhcmtlciIsInVwZGF0ZUF4aXMiLCJfcmVmNyIsImF4aXNMZWZ0IiwiYXhpc0JvdHRvbSIsInRpY2tzIiwibWF4IiwiZmxvb3IiLCJvZmZzZXQiLCJidWZmZXIiLCJjZWlsIiwieDIiLCJzY2FsZVF1YW50aWxlIiwiYXJndW1lbnRzIiwibGVuZ3RoIiwiZGF0dW0iLCJ1cGRhdGVTY2FsZXMiLCJoYW5kbGVJbnB1dCIsIndlaWdodCIsInRoaXMiLCJ2YWx1ZSIsInZhbCIsIndlaWdodGVkIiwiaW5pdCIsImdyYXBoaWMiLCJvbiIsInJhbmRvbSJdLCJtYXBwaW5ncyI6IllBSzRDLFNBQU5BLFlBQU1DLEdBQU4sR0FBQUMsR0FBQUQsRUFBQUMsRUFBQUMsRUFBQUYsRUFBQUUsQ0FBcEIsT0FBbEJDLFdBQUFDLElBQUEsU0FBQUMsR0FBQSxNQUFBQyxhQVdLRCxHQUxMRSxNQUFNQyxFQUFBQSxFQUFRQyxFQUFBQSxFQUFBQSxFQUFkUCxNQVFFUSxLQUFLLFNBQUNDLEVBQUdDLEdBQUosTUFBVUMsSUFBR0MsV0FBV0gsRUFBRUosTUFBT0ssRUFBRUwsU0FMMUNILElBQUEsU0FBQUMsRUFBU04sR0FBVCxNQUFBTyxhQUFzQkwsR0FBUWMsS0FBQUMsTUFDN0JDLFVBQXFCLFFBQUFDLGVBQUFDLEdBQUEsR0FBQWxCLEdBQUFrQixFQUFBbEIsRUFBQUMsRUFBQWlCLEVBQUFqQixFQUlwQlEsRUFBS1QsRUFBQUEsRUFBQW1CLEVBQWFOLEVBQUFBLENBQWIsT0FDRE8sTUFBQUMsS0FBQ2pCLEVBQURlLEdBQUEsUUFBQUcsVUFBQSxHQUlKTixHQVRELEdBQU9JLEtBQVBHLElBQUFDLEdBQUFDLE9BQUFDLFlBQUFDLE9BQUFDLFlBVUFyQixPQUFBc0IsTUFBQUMsR0FBQUMsT0FBQUQsR0FXQU4sR0FBR1EsS0FBS3pCLE9BVHdCLFFBQUFDLGVBY2hDLFFBQU15QixHQUFZQyxFQUFBQSxHQUNsQixNQUFBLGFBQWtCQSxFQUFsQixLQUFlakMsRUFBZixJQWdCQSxRQUFTa0MsR0FBVEMsR0FBb0MsR0FBbkJDLEdBQW1CRCxFQUFuQkMsVUFBV0MsRUFBUUYsRUFBUkUsS0FaeEJULEVBQUpRLEVBQUFFLFVBQUEsT0FBQUQsTUFBQUEsSUFDSVAsRUFBSlMsRUFBQUws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QU15RCxHQUFBQSxLQUFBQSxVQUFORSxRQUFBLEdBQ0ExRCxFQUFNMEQsR0FBQUEsS0FBVXNELFVBQWhCdEQsUUFBQSxNQUVDdUIsTUFEMkMxRSxjQUV4Q2dCLEdBQU1KLFFBQUs2RixPQUFMLFNBNk1UMUciLCJmaWxlIjoic2NyaXB0LmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgZ3JhcGhpYyA9IGQzLnNlbGVjdCgnLmdyYXBoaWMtLTMnKVxuXG5jb25zdCBDT0xPUlMgPSBbJyNmZjM4MTQnLCAnI2ZlNWMzNCcsICcjZmM3NjRmJywgJyNmODhkNjknLCAnI2YyYTM4NScsICcjZThiOGEwJywgJyNkYmNkYmQnXVxuY29uc3QgRk9OVF9TSVpFID0gMTFcbmNvbnN0IE1BWF9WQUwgPSAxMFxuY29uc3QgZHVtbXlEYXRhID0gZDMucmFuZ2UoMCwgNTApLm1hcChkID0+ICh7XG5cdGluZGV4OiBkLFxuXHR4OiAwLjUgKyBNYXRoLnJhbmRvbSgpICogKE1BWF9WQUwgLSAxKSxcblx0eTogMC41ICsgTWF0aC5yYW5kb20oKSAqIChNQVhfVkFMIC0gMSksXG59KSlcblxuY29uc3QgY2hhcnQgPSBzY2F0dGVycGxvdCgpXG5jb25zdCBlbCA9IGdyYXBoaWMuc2VsZWN0KCcuY2hhcnQnKVxuXG5mdW5jdGlvbiB3ZWlnaHREYXRhKHsgeCwgeSB9KSB7XG5cdHJldHVybiBkdW1teURhdGEubWFwKGQgPT4gKHtcblx0XHQuLi5kLFxuXHRcdHNjb3JlOiBkLnggKiB4ICsgZC55ICogeSxcblx0fSkpXG5cdC5zb3J0KChhLCBiKSA9PiBkMy5kZXNjZW5kaW5nKGEuc2NvcmUsIGIuc2NvcmUpKVxuXHQubWFwKChkLCBpKSA9PiAoe1xuXHRcdC4uLmQsXG5cdFx0cmFuazogaSxcblx0fSkpXG5cdC5yZXZlcnNlKClcbn1cblxuZnVuY3Rpb24gZ2V0SHlwb3RlbnVzZSh7IHgsIHkgfSkge1xuXHRjb25zdCB4MiA9IHggKiB4XG5cdGNvbnN0IHkyID0geSAqIHlcblx0cmV0dXJuIE1hdGguc3FydCh4MiArIHkyKVxufVxuXG5mdW5jdGlvbiByZXNpemUoKSB7XG5cdGNvbnN0IHN6ID0gTWF0aC5taW4oZWwubm9kZSgpLm9mZnNldFdpZHRoLCB3aW5kb3cuaW5uZXJIZWlnaHQpICogMC44XG5cdGNoYXJ0LndpZHRoKHN6KS5oZWlnaHQoc3opXG5cdGVsLmNhbGwoY2hhcnQpXG59XG5cbmZ1bmN0aW9uIHNjYXR0ZXJwbG90KCkge1xuXHRjb25zdCBtYXJnaW4gPSBGT05UX1NJWkUgKiAzXG5cdGNvbnN0IHNjYWxlWCA9IGQzLnNjYWxlTGluZWFyKClcblx0Y29uc3Qgc2NhbGVZID0gZDMuc2NhbGVMaW5lYXIoKVxuXHRjb25zdCBzY2FsZVIgPSBkMy5zY2FsZVNxcnQoKVxuXHRjb25zdCBzY2FsZUMgPSBkMy5zY2FsZVF1YW50aWxlKClcblxuXHRsZXQgd2lkdGggPSAwXG5cdGxldCBoZWlnaHQgPSAwXG5cdGxldCBjaGFydFdpZHRoID0gMFxuXHRsZXQgY2hhcnRIZWlnaHQgPSAwXG5cdGxldCB3ZWlnaHRYID0gNTBcblx0bGV0IHdlaWdodFkgPSA1MFxuXHRsZXQgaHlwb3RlbnVzZSA9IDBcblxuXHRmdW5jdGlvbiB0cmFuc2xhdGUoeCwgeSkge1xuXHRcdHJldHVybiBgdHJhbnNsYXRlKCR7eH0sICR7eX0pYFxuXHR9XG5cblx0ZnVuY3Rpb24gZW50ZXIoeyBjb250YWluZXIsIGRhdGEgfSkge1xuXHRcdGNvbnN0IHN2ZyA9IGNvbnRhaW5lci5zZWxlY3RBbGwoJ3N2ZycpLmRhdGEoW2RhdGFdKVxuXHRcdGNvbnN0IHN2Z0VudGVyID0gc3ZnLmVudGVyKCkuYXBwZW5kKCdzdmcnKVxuICAgICAgXHRjb25zdCBnRW50ZXIgPSBzdmdFbnRlci5hcHBlbmQoJ2cnKVxuXHRcdFxuXHRcdGdFbnRlci5hcHBlbmQoJ2cnKS5hdHRyKCdjbGFzcycsICdnLXBsb3QnKVxuXG5cdFx0Y29uc3QgYXhpcyA9IGdFbnRlci5hcHBlbmQoJ2cnKS5hdHRyKCdjbGFzcycsICdnLWF4aXMnKVxuXG5cdFx0Y29uc3QgeCA9IGF4aXMuYXBwZW5kKCdnJykuYXR0cignY2xhc3MnLCAnYXhpcyBheGlzLS14JylcblxuXHRcdGNvbnN0IHkgPSBheGlzLmFwcGVuZCgnZycpLmF0dHIoJ2NsYXNzJywgJ2F4aXMgYXhpcy0teScpXG5cblx0XHR4LmFwcGVuZCgndGV4dCcpLmF0dHIoJ2NsYXNzJywgJ2F4aXNfX2xhYmVsJylcblx0XHRcdC5hdHRyKCd0ZXh0LWFuY2hvcicsICdzdGFydCcpXG5cdFx0XHQudGV4dCgnUXVhbnRpdHknKVxuXG5cdFx0eS5hcHBlbmQoJ3RleHQnKS5hdHRyKCdjbGFzcycsICdheGlzX19sYWJlbCcpXG5cdFx0XHQuYXR0cigndGV4dC1hbmNob3InLCAnZW5kJylcblx0XHRcdC50ZXh0KCdRdWFsaXR5JylcdFxuXHR9XG5cblx0ZnVuY3Rpb24gZXhpdCh7IGNvbnRhaW5lciwgZGF0YSB9KSB7XG5cdH1cblxuXHRmdW5jdGlvbiB1cGRhdGVTY2FsZXMoeyBkYXRhIH0pIHtcblx0XHRoeXBvdGVudXNlID0gZ2V0SHlwb3RlbnVzZSh7IHg6IHdlaWdodFgsIHk6IHdlaWdodFkgfSlcblx0XHRjb25zdCByYW5nZVggPSB3ZWlnaHRYIC8gaHlwb3RlbnVzZSAqIGNoYXJ0V2lkdGhcblx0XHRjb25zdCByYW5nZVkgPSB3ZWlnaHRZIC8gaHlwb3RlbnVzZSAqIGNoYXJ0SGVpZ2h0XG5cdFx0Y29uc3QgbWF4UiA9IE1hdGguZmxvb3IoRk9OVF9TSVpFICogMS41KVxuXG5cdFx0c2NhbGVYXG5cdFx0XHQuZG9tYWluKFswLCBNQVhfVkFMXSlcblx0XHRcdC5yYW5nZShbMCwgcmFuZ2VYXSlcblxuXHRcdHNjYWxlWVxuXHRcdFx0LmRvbWFpbihbMCwgTUFYX1ZBTF0pXG5cdFx0XHQucmFuZ2UoW3JhbmdlWSwgMF0pXG5cblx0XHRzY2FsZVJcblx0XHRcdC5kb21haW4oWzAsIGRhdGEubGVuZ3RoXSlcblx0XHRcdC5yYW5nZShbbWF4UiwgMl0pXG5cblx0XHRzY2FsZUNcblx0XHRcdC5kb21haW4oZGF0YS5tYXAoZCA9PiBkLnJhbmspKVxuXHRcdFx0LnJhbmdlKENPTE9SUylcblx0fVxuXG5cdGZ1bmN0aW9uIHVwZGF0ZURvbSh7IGNvbnRhaW5lciwgZGF0YSB9KSB7XG5cdFx0Y29uc3Qgc3ZnID0gY29udGFpbmVyLnNlbGVjdCgnc3ZnJylcblx0XHRcblx0XHRzdmdcblx0XHRcdC5hdHRyKCd3aWR0aCcsIHdpZHRoKVxuXHRcdFx0LmF0dHIoJ2hlaWdodCcsIGhlaWdodClcblxuXHRcdGNvbnN0IGcgPSBzdmcuc2VsZWN0KCdnJylcblx0XHRcblx0XHRjb25zdCBtYXhZID0gc2NhbGVZLnJhbmdlKClbMF1cblx0XHRjb25zdCBvZmZzZXRYID0gY2hhcnRXaWR0aCAvIDJcblx0XHRjb25zdCBvZmZzZXRZID0gY2hhcnRIZWlnaHQgLSBtYXhZXG5cdFx0Y29uc3QgcmFkID0gTWF0aC5hY29zKHdlaWdodFggLyBoeXBvdGVudXNlKVxuXHRcdGNvbnN0IGFuZ2xlID0gOTAgLSAocmFkICogMTgwIC8gTWF0aC5QSSlcblx0XHRjb25zdCByb3RhdGlvbiA9IGByb3RhdGUoJHstYW5nbGV9IDAgJHtzY2FsZVkucmFuZ2UoKVswXX0pYFxuXHRcdGNvbnN0IHRyYW5zbGF0aW9uID0gdHJhbnNsYXRlKG1hcmdpbiAqIDEuNSArIG9mZnNldFgsIG1hcmdpbiArIG9mZnNldFkpXG5cdFx0Y29uc3QgdHJhbnNmb3JtID0gYCR7dHJhbnNsYXRpb259ICR7cm90YXRpb259YFxuXHRcdGcuYXR0cigndHJhbnNmb3JtJywgdHJhbnNmb3JtKVxuXG5cdFx0Y29uc3QgcGxvdCA9IGcuc2VsZWN0KCcuZy1wbG90JylcblxuXHRcdGNvbnN0IGl0ZW0gPSBwbG90LnNlbGVjdEFsbCgnLml0ZW0nKS5kYXRhKGQgPT4gZCwgZCA9PiBkLmluZGV4KVxuXHRcdFxuXHRcdGl0ZW0uZW50ZXIoKS5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0XHQuYXR0cignY2xhc3MnLCAnaXRlbScpXG5cdFx0Lm1lcmdlKGl0ZW0pXG5cdFx0XHQuYXR0cigneCcsIDApXG5cdFx0XHQuYXR0cigneScsIDApXG5cdFx0XHQuYXR0cigncicsIGQgPT4gc2NhbGVSKGQucmFuaykpXG5cdFx0XHQuc3R5bGUoJ2ZpbGwnLCBkID0+IHNjYWxlQyhkLnJhbmspKVxuXHRcdFx0LnN0eWxlKCdzdHJva2UnLCBkID0+IGQzLmNvbG9yKHNjYWxlQyhkLnJhbmspKS5kYXJrZXIoMC43KSlcblx0XHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCAgZCA9PiB0cmFuc2xhdGUoc2NhbGVYKGQueCksIHNjYWxlWShkLnkpKSlcblx0fVxuXG5cdGZ1bmN0aW9uIHVwZGF0ZUF4aXMoeyBjb250YWluZXIsIGRhdGEgfSkge1xuXHRcdGNvbnN0IGF4aXMgPSBjb250YWluZXIuc2VsZWN0KCcuZy1heGlzJylcblxuXHRcdGNvbnN0IGF4aXNMZWZ0ID0gZDMuYXhpc0xlZnQoc2NhbGVZKVxuXHRcdGNvbnN0IGF4aXNCb3R0b20gPSBkMy5heGlzQm90dG9tKHNjYWxlWClcblxuXHRcdGF4aXNMZWZ0LnRpY2tzKE1hdGgubWF4KDAsIE1hdGguZmxvb3Iod2VpZ2h0WSAvIDEwKSkpXG5cdFx0YXhpc0JvdHRvbS50aWNrcyhNYXRoLm1heCgwLCBNYXRoLmZsb29yKHdlaWdodFggLyAxMCkpKVxuXHRcdGNvbnN0IHggPSBheGlzLnNlbGVjdCgnLmF4aXMtLXgnKVxuXHRcdFxuXHRcdGNvbnN0IG1heFkgPSBzY2FsZVkucmFuZ2UoKVswXVxuXHRcdGNvbnN0IG9mZnNldCA9IG1heFlcblxuXHRcdGNvbnN0IGJ1ZmZlciA9IE1hdGguY2VpbChtYXJnaW4gLyAyKVxuXHRcdHguYXR0cigndHJhbnNmb3JtJywgdHJhbnNsYXRlKDAsIGJ1ZmZlciArIG9mZnNldCkpXG5cdFx0XHQuY2FsbChheGlzQm90dG9tKVxuXG5cdFx0Y29uc3QgeSA9IGF4aXMuc2VsZWN0KCcuYXhpcy0teScpXG5cblx0XHR5LmF0dHIoJ3RyYW5zZm9ybScsIHRyYW5zbGF0ZSgtYnVmZmVyLCAwKSlcblx0XHRcdC5jYWxsKGF4aXNMZWZ0KVxuXG5cdFx0eC5zZWxlY3QoJy5heGlzX19sYWJlbCcpXG5cdFx0XHQuYXR0cigneScsIG1hcmdpbiAtIDEpXG5cblx0XHR5LnNlbGVjdCgnLmF4aXNfX2xhYmVsJylcblx0XHRcdC5hdHRyKCd4Jywgb2Zmc2V0KVxuXHRcdFx0LmF0dHIoJ3knLCBtYXJnaW4gLSAxKVxuXHRcdFx0LmF0dHIoJ3RyYW5zZm9ybScsIGByb3RhdGUoOTApYClcblx0fVxuXG5cdGZ1bmN0aW9uIGNoYXJ0KGNvbnRhaW5lcikge1xuXHRcdGNvbnN0IGRhdGEgPSBjb250YWluZXIuZGF0dW0oKVxuXHRcdFxuXHRcdGVudGVyKHsgY29udGFpbmVyLCBkYXRhIH0pXG5cdFx0ZXhpdCh7IGNvbnRhaW5lciwgZGF0YSB9KVxuXHRcdHVwZGF0ZVNjYWxlcyh7IGNvbnRhaW5lciwgZGF0YSB9KVxuXHRcdHVwZGF0ZURvbSh7IGNvbnRhaW5lciwgZGF0YSB9KVxuXHRcdHVwZGF0ZUF4aXMoeyBjb250YWluZXIsIGRhdGEgfSlcblx0fVxuXG5cdGNoYXJ0LndpZHRoID0gZnVuY3Rpb24oLi4uYXJncykge1xuXHRcdGlmICghYXJncy5sZW5ndGgpIHJldHVybiB3aWR0aFxuXHRcdHdpZHRoID0gYXJnc1swXVxuXHRcdGNoYXJ0V2lkdGggPSB3aWR0aCAtIG1hcmdpbiAqIDIuNVxuXHRcdHJldHVybiBjaGFydFxuXHR9XG5cblx0Y2hhcnQuaGVpZ2h0ID0gZnVuY3Rpb24oLi4uYXJncykge1xuXHRcdGlmICghYXJncy5sZW5ndGgpIHJldHVybiBoZWlnaHRcblx0XHRoZWlnaHQgPSBhcmdzWzBdXG5cdFx0Y2hhcnRIZWlnaHQgPSBoZWlnaHQgLSBtYXJnaW4gKiAyLjVcblx0XHRyZXR1cm4gY2hhcnRcblx0fVxuXG5cdGNoYXJ0LndlaWdodCA9IGZ1bmN0aW9uKHsgeCwgeSB9KSB7XG5cdFx0d2VpZ2h0WCA9IHhcblx0XHR3ZWlnaHRZID0geVxuXHRcdHJldHVybiBjaGFydFxuXHR9XG5cblxuXG5cdHJldHVybiBjaGFydFxufVxuXG5mdW5jdGlvbiBoYW5kbGVJbnB1dCgpIHtcblx0Y29uc3QgdmFsID0gK3RoaXMudmFsdWVcblx0Y29uc3QgeCA9IHZhbFxuXHRjb25zdCB5ID0gMTAwIC0gdmFsXG5cdGNvbnN0IHdlaWdodGVkID0gd2VpZ2h0RGF0YSh7IHgsIHkgfSlcblxuXHRjaGFydC53ZWlnaHQoeyB4LCB5IH0pXG5cdGVsLmRhdHVtKHdlaWdodGVkKVxuXHRlbC5jYWxsKGNoYXJ0KVxufVxuXG5mdW5jdGlvbiBpbml0KCkge1xuXHRlbC5kYXR1bSh3ZWlnaHREYXRhKHsgeDogNTAsIHk6IDUwIH0pKVxuXHRlbC5jYWxsKGNoYXJ0KVxuXHRyZXNpemUoKVxuXHR3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcigncmVzaXplJywgcmVzaXplKVxuXHRncmFwaGljLnNlbGVjdCgnLnNsaWRlciBpbnB1dCcpLm9uKCdpbnB1dCcsIGhhbmRsZUlucHV0KVxufVxuXG5pbml0KClcblxuIl19 |
| <!DOCTYPE html> | |
| <title>weighted pivot scatter plot v3</title> | |
| <link href='dist.css' rel='stylesheet' /> | |
| <body> | |
| <div class='graphic graphic--3'> | |
| <div class='chart'></div> | |
| <div class='slider'><input type='range' min='0' max='100' value='50'></div> | |
| </div> | |
| <script src='https://d3js.org/d3.v4.min.js'></script> | |
| <script src='dist.js'></script> | |
| </body> |
| const graphic = d3.select('.graphic--3') | |
| const COLORS = ['#f7f7f7','#d9d9d9','#bdbdbd','#969696','#737373','#525252','#252525'] | |
| const FONT_SIZE = 11 | |
| const MAX_VAL = 10 | |
| const dummyData = d3.range(0, 50).map(d => ({ | |
| index: d, | |
| x: 0.5 + Math.random() * (MAX_VAL - 1), | |
| y: 0.5 + Math.random() * (MAX_VAL - 1), | |
| })) | |
| const chart = scatterplot() | |
| const el = graphic.select('.chart') | |
| function weightData({ x, y }) { | |
| return dummyData.map(d => ({ | |
| ...d, | |
| score: d.x * x + d.y * y, | |
| })) | |
| .sort((a, b) => d3.descending(a.score, b.score)) | |
| .map((d, i) => ({ | |
| ...d, | |
| rank: i, | |
| })) | |
| .reverse() | |
| } | |
| function getHypotenuse({ x, y }) { | |
| const x2 = x * x | |
| const y2 = y * y | |
| return Math.sqrt(x2 + y2) | |
| } | |
| function resize() { | |
| const sz = Math.min(el.node().offsetWidth, window.innerHeight) * 0.8 | |
| chart.width(sz).height(sz) | |
| el.call(chart) | |
| } | |
| function scatterplot() { | |
| const margin = FONT_SIZE * 3 | |
| const scaleX = d3.scaleLinear() | |
| const scaleY = d3.scaleLinear() | |
| const scaleR = d3.scaleSqrt() | |
| const scaleC = d3.scaleQuantile() | |
| let width = 0 | |
| let height = 0 | |
| let chartWidth = 0 | |
| let chartHeight = 0 | |
| let weightX = 50 | |
| let weightY = 50 | |
| let hypotenuse = 0 | |
| function translate(x, y) { | |
| return `translate(${x}, ${y})` | |
| } | |
| function enter({ container, data }) { | |
| const svg = container.selectAll('svg').data([data]) | |
| const svgEnter = svg.enter().append('svg') | |
| const gEnter = svgEnter.append('g') | |
| gEnter.append('g').attr('class', 'g-plot') | |
| const axis = gEnter.append('g').attr('class', 'g-axis') | |
| const x = axis.append('g').attr('class', 'axis axis--x') | |
| const y = axis.append('g').attr('class', 'axis axis--y') | |
| x.append('text').attr('class', 'axis__label') | |
| .attr('text-anchor', 'start') | |
| .text('Quantity') | |
| y.append('text').attr('class', 'axis__label') | |
| .attr('text-anchor', 'end') | |
| .text('Quality') | |
| } | |
| function exit({ container, data }) { | |
| } | |
| function updateScales({ data }) { | |
| hypotenuse = getHypotenuse({ x: weightX, y: weightY }) | |
| const rangeX = weightX / hypotenuse * chartWidth | |
| const rangeY = weightY / hypotenuse * chartHeight | |
| const maxR = Math.floor(FONT_SIZE * 1.5) | |
| scaleX | |
| .domain([0, MAX_VAL]) | |
| .range([0, rangeX]) | |
| scaleY | |
| .domain([0, MAX_VAL]) | |
| .range([rangeY, 0]) | |
| scaleR | |
| .domain([0, data.length]) | |
| .range([maxR, 2]) | |
| scaleC | |
| .domain(data.map(d => d.rank)) | |
| .range(COLORS) | |
| } | |
| function updateDom({ container, data }) { | |
| const svg = container.select('svg') | |
| svg | |
| .attr('width', width) | |
| .attr('height', height) | |
| const g = svg.select('g') | |
| const maxY = scaleY.range()[0] | |
| const offsetX = chartWidth / 2 | |
| const offsetY = chartHeight - maxY | |
| const rad = Math.acos(weightX / hypotenuse) | |
| const angle = 90 - (rad * 180 / Math.PI) | |
| const rotation = `rotate(${-angle} 0 ${scaleY.range()[0]})` | |
| const translation = translate(margin * 1.5 + offsetX, margin + offsetY) | |
| const transform = `${translation} ${rotation}` | |
| g.attr('transform', transform) | |
| const plot = g.select('.g-plot') | |
| const item = plot.selectAll('.item').data(d => d, d => d.index) | |
| item.enter().append('circle') | |
| .attr('class', 'item') | |
| .merge(item) | |
| .attr('x', 0) | |
| .attr('y', 0) | |
| .attr('r', d => scaleR(d.rank)) | |
| .style('fill', d => scaleC(d.rank)) | |
| .style('stroke', d => d3.color(scaleC(d.rank)).darker(0.7)) | |
| .attr('transform', d => translate(scaleX(d.x), scaleY(d.y))) | |
| } | |
| function updateAxis({ container, data }) { | |
| const axis = container.select('.g-axis') | |
| const axisLeft = d3.axisLeft(scaleY) | |
| const axisBottom = d3.axisBottom(scaleX) | |
| axisLeft.ticks(Math.max(0, Math.floor(weightY / 10))) | |
| axisBottom.ticks(Math.max(0, Math.floor(weightX / 10))) | |
| const x = axis.select('.axis--x') | |
| const maxY = scaleY.range()[0] | |
| const offset = maxY | |
| const buffer = Math.ceil(margin / 2) | |
| x.attr('transform', translate(0, buffer + offset)) | |
| .call(axisBottom) | |
| const y = axis.select('.axis--y') | |
| y.attr('transform', translate(-buffer, 0)) | |
| .call(axisLeft) | |
| x.select('.axis__label') | |
| .attr('y', margin - 1) | |
| y.select('.axis__label') | |
| .attr('x', offset) | |
| .attr('y', margin - 1) | |
| .attr('transform', `rotate(90)`) | |
| } | |
| function chart(container) { | |
| const data = container.datum() | |
| enter({ container, data }) | |
| exit({ container, data }) | |
| updateScales({ container, data }) | |
| updateDom({ container, data }) | |
| updateAxis({ container, data }) | |
| } | |
| chart.width = function(...args) { | |
| if (!args.length) return width | |
| width = args[0] | |
| chartWidth = width - margin * 2.5 | |
| return chart | |
| } | |
| chart.height = function(...args) { | |
| if (!args.length) return height | |
| height = args[0] | |
| chartHeight = height - margin * 2.5 | |
| return chart | |
| } | |
| chart.weight = function({ x, y }) { | |
| weightX = x | |
| weightY = y | |
| return chart | |
| } | |
| return chart | |
| } | |
| function handleInput() { | |
| const val = +this.value | |
| const x = val | |
| const y = 100 - val | |
| const weighted = weightData({ x, y }) | |
| chart.weight({ x, y }) | |
| el.datum(weighted) | |
| el.call(chart) | |
| } | |
| function init() { | |
| el.datum(weightData({ x: 50, y: 50 })) | |
| el.call(chart) | |
| resize() | |
| window.addEventListener('resize', resize) | |
| graphic.select('.slider input').on('input', handleInput) | |
| } | |
| init() | |
| $gray = #888 | |
| $gray-light = #aaa | |
| $gray-lighter = #ccc | |
| $gray-dark = #333 | |
| $sans = Helvetica, Arial, sans-serif | |
| $small-font = 11px | |
| * | |
| box-sizing border-box | |
| body | |
| color $gray-dark | |
| font-family $sans | |
| .graphic__hed | |
| text-align center | |
| color $gray-dark | |
| .chart | |
| max-width 40rem | |
| margin 0 auto | |
| text-align center | |
| .slider | |
| max-width 20rem | |
| margin 1rem auto | |
| position relative | |
| padding-top $small-font * 1.5 | |
| input | |
| display block | |
| width 100% | |
| &:before | |
| content 'Quality' | |
| text-transform uppercase | |
| font-size $small-font | |
| letter-spacing 0.05em | |
| color $gray | |
| display inline-block | |
| position absolute | |
| top 0 | |
| left 0 | |
| &:after | |
| content 'Quantity' | |
| text-transform uppercase | |
| font-size $small-font | |
| letter-spacing 0.05em | |
| color $gray | |
| display inline-block | |
| position absolute | |
| top 0 | |
| right 0 | |
| .g-axis | |
| line, path | |
| stroke $gray-lighter | |
| text | |
| fill $gray | |
| .g-axis .axis__label | |
| text-transform uppercase | |
| font-size $small-font | |
| letter-spacing 0.05em | |
| fill $gray | |
| .item | |
| fill-opacity 0.75 | |