Created
October 19, 2020 18:41
-
-
Save valex/45eb5924396fd16227c2e63b304a6141 to your computer and use it in GitHub Desktop.
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> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="stylesheet" href="styles.css"> | |
</head> | |
<body> | |
<div style="display: flex; | |
flex-direction: row; justify-content: space-between;"> | |
<div id="chart"></div> | |
<div id="chart_right"></div> | |
</div> | |
</body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js" ></script> | |
<script src="scripts.js"></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
function CHART_1_class(id, selector){ | |
this.options = { | |
selector: selector, | |
viewBox: [400, 300], | |
bands: { | |
left: ["pinky", "ring", "middle", "pointing"], | |
right: ["pointing", "middle", "ring", "pinky"] | |
}, | |
colors: { | |
background: '#d7e5ec', | |
better: '#4bc774', | |
worse: 'red', | |
}, | |
groups: ['onPeriodStart', 'onPeriodEnd'], | |
yScaleDomain: [0, 160], | |
paddings: { | |
main: { | |
left: 0.2, | |
bottom: 0.3 | |
} | |
}, | |
headerHeight: 0.18 | |
} | |
this.id = id, | |
this.el = null, | |
this.aspect = null, | |
this.originalData = null, | |
this.data = null, | |
this.vis = { | |
svg: null, | |
defs: null, | |
background:null, | |
legends: null, | |
hand_legends: null, | |
yAxis: null, | |
markers:null, | |
bars: null, | |
mainRect: { // init values | |
x: 0, | |
y: 0, | |
width: this.options.viewBox[0], | |
height: this.options.viewBox[1] | |
}, | |
headerRect: { | |
x: null, | |
y: null, | |
width: null, | |
height: null, | |
center: { | |
x: null, | |
y: null | |
} | |
}, | |
xScale: null, | |
yScale: null | |
} | |
this.el = d3.select(this.options.selector); | |
this.aspect = this.options.viewBox[0] / this.options.viewBox[1]; | |
window.addEventListener("resize", this.onResize.bind(this), false); | |
}; | |
CHART_1_class.prototype = { | |
setData: function(data){ | |
this.originalData = Object.assign({}, data); | |
this.data = null; | |
this.prepareData(); | |
}, | |
prepareData: function(){ | |
var that = this; | |
this.data = Object.assign({}, this.originalData); | |
// build diff | |
this.data['diff'] = {}; | |
for (var prop in this.data[this.options.groups[1]]) { | |
if( ! this.data[this.options.groups[1]].hasOwnProperty( prop ) ) | |
continue; | |
if( typeof this.data[this.options.groups[0]][prop] === 'undefined' ) | |
continue; | |
this.data['diff'][prop] = this.data[this.options.groups[1]][prop] - this.data[this.options.groups[0]][prop]; | |
} | |
var diff_entries = Object.entries(this.data['diff']); | |
this.data['diff_max_key'] = diff_entries[ | |
d3.maxIndex(diff_entries, function(d){ | |
return d[1]; | |
}) | |
][0]; | |
this.data['diff_min_key'] = diff_entries[ | |
d3.minIndex(diff_entries, function(d){ | |
return d[1]; | |
}) | |
][0]; | |
for(var i=0; i < this.options.groups.length; i++){ | |
var group = this.options.groups[i]; | |
// calculate min and max | |
for (var prop in this.data[group]) { | |
if( this.data[group].hasOwnProperty( prop ) ) { | |
if(typeof this.data['maxValue'] === 'undefined'){ | |
this.data['maxValue'] = +this.data[group][prop]; | |
}else{ | |
if(+this.data[group][prop] > this.data['maxValue']) | |
this.data['maxValue'] = +this.data[group][prop]; | |
} | |
if(typeof this.data['minValue'] === 'undefined'){ | |
this.data['minValue'] = +this.data[group][prop]; | |
}else{ | |
if(+this.data[group][prop] < this.data['minValue']) | |
this.data['minValue'] = +this.data[group][prop]; | |
} | |
} | |
} | |
// build entries | |
this.data[group]['entries'] = Object.entries(this.data[group]) | |
} | |
console.log(this.data); | |
}, | |
buildVis: function(){ | |
if( ! this.vis.svg ) this.buildUnchanged(); | |
this.makeCalculations(); | |
this.buildSkeleton(); | |
this.buildLegends(); | |
this.buildAxes(); | |
this.buildMarkers(); | |
this.buildBars(); | |
}, | |
makeCalculations: function(){ | |
var headerPlusMainHeigh = Math.ceil(this.options.viewBox[1] - (this.options.paddings.main.bottom * this.options.viewBox[1])); | |
this.vis.headerRect.x = 1 + Math.round(this.options.paddings.main.left * this.options.viewBox[0]); | |
this.vis.headerRect.y = 1; | |
this.vis.headerRect.width = this.options.viewBox[0] - this.vis.headerRect.x -1; | |
this.vis.headerRect.height = Math.floor( this.options.headerHeight * headerPlusMainHeigh); | |
this.vis.headerRect.center.x = this.vis.headerRect.x + this.vis.headerRect.width / 2; | |
this.vis.headerRect.center.y = this.vis.headerRect.y + this.vis.headerRect.height / 2; | |
this.vis.mainRect.x = this.vis.headerRect.x; | |
this.vis.mainRect.y = this.vis.headerRect.y + this.vis.headerRect.height; | |
this.vis.mainRect.width = this.vis.headerRect.width; | |
this.vis.mainRect.height = Math.floor(headerPlusMainHeigh - this.vis.headerRect.height); | |
this.vis.xScale = d3.scaleBand() | |
.domain(this.options.bands[this.data['hand']]) | |
.rangeRound([this.vis.mainRect.x, this.vis.mainRect.x+this.vis.mainRect.width]) | |
.paddingInner(0.2) | |
.paddingOuter(0.3); | |
this.vis.yScale = d3.scaleLinear() | |
.domain(this.options.yScaleDomain) | |
.range([ | |
this.vis.mainRect.y + this.vis.mainRect.height, | |
this.vis.mainRect.y | |
]); | |
}, | |
buildMarkers:function(){ | |
var that = this; | |
var text_boundings = []; | |
this.vis['markers'] | |
.selectAll('text') | |
.data([that.data.diff_max_key, that.data.diff_min_key]) | |
.join("text") | |
.attr("x", function(){return 10}) | |
.attr("y", function(d){return that.vis.yScale(that.data[that.options.groups[1]][d]) }) | |
.attr("dx", 10) | |
.attr("dy", 1) | |
.attr("font-size", "1rem") | |
.attr("fill", "black") | |
.attr("text-anchor", "start") | |
.style("alignment-baseline", "middle") | |
.style("font-weight", "normal") | |
.attr("class", "noselect") | |
.text(function(d, i) { | |
return that.data[that.options.groups[1]][d]+"\u00B0"; | |
}) | |
.each(function(){ | |
text_boundings.push(d3.select(this).node().getBBox()); | |
}) | |
var rect_padding = 4; | |
var rect_boundings = []; | |
this.vis['markers'] | |
.selectAll('rect') | |
.data(text_boundings) | |
.join("rect") | |
.attr('rx', 6) | |
.attr('ry', 6) | |
.attr('x', function(d){ return d.x - rect_padding }) | |
.attr('y', function(d){ return d.y -rect_padding }) | |
.attr('width', function(d){ return d.width + 2*rect_padding }) | |
.attr('height', function(d){ return d.height +2*rect_padding }) | |
.attr('fill', 'white') | |
.style('fill-opacity', "0") | |
.style('stroke-width', '1px') | |
.style('stroke', function(d, i){ | |
if(i===0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.each(function(){ | |
rect_boundings.push(d3.select(this).node().getBBox()); | |
}) | |
this.vis['markers'].selectAll('line') | |
.data([that.data.diff_max_key, that.data.diff_min_key]) | |
.join("line") | |
.attr('x1', function(d, i){ | |
return that.vis.xScale(d) + that.vis.xScale.bandwidth() /2; | |
}) | |
.attr('y1', function(d, i){ | |
return that.vis.yScale(that.data[that.options.groups[1]][d]) | |
}) | |
.attr('x2', function(d, i){ return rect_boundings[i].x + rect_boundings[i].width }) | |
.attr('y2', function(d, i){ | |
return that.vis.yScale(that.data[that.options.groups[1]][d]) | |
}) | |
.style('stroke-width', '1px') | |
.style('stroke', function(d, i){ | |
if(i===0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
}, | |
buildLegends: function(){ | |
var that = this; | |
var handMarker = this.vis.legends | |
.selectAll('text') | |
.data([that.data.hand]) | |
.join("text") | |
.attr("x", function(){return that.vis.headerRect.x;}) | |
.attr("y", function(){return that.vis.headerRect.center.y;}) | |
.attr("dx", 10) | |
.attr("dy", 2) | |
.attr("font-size", "1rem") | |
.attr("fill", "black") | |
.attr("text-anchor", "start") | |
.style("alignment-baseline", "middle") | |
.style("font-weight", "bold") | |
.attr("class", "noselect") | |
.text(function(d) { | |
switch(d){ | |
case 'left': | |
return "Левая рука" | |
break; | |
default: | |
return "Правая рука" | |
break; | |
} | |
}); | |
this.vis.legends | |
.selectAll('circle') | |
.data([0, 1]) | |
.enter() | |
.append("circle") | |
.attr("cx", function(d){ | |
switch(d){ | |
case 0: | |
return that.vis.headerRect.center.x; | |
break; | |
default: | |
return that.vis.headerRect.center.x + 90; | |
break; | |
} | |
}) | |
.attr("cy", that.vis.headerRect.center.y) | |
.attr("r", function(){return 0.3 * that.vis.headerRect.height / 2}) | |
.attr("fill", function(d){ | |
switch(d){ | |
case 0: | |
return that.options.colors.better; | |
break; | |
default: | |
return that.options.colors.worse; | |
break; | |
} | |
}); | |
this.vis.legends | |
.selectAll('text.legend-text') | |
.data([0, 1]) | |
.enter() | |
.append('text') | |
.classed("legend-text", true) | |
.attr("x", function(d){ | |
switch(d){ | |
case 0: | |
return that.vis.headerRect.center.x; | |
break; | |
default: | |
return that.vis.headerRect.center.x + 90; | |
break; | |
} | |
}) | |
.attr("y", that.vis.headerRect.center.y) | |
.attr("dx", 10) | |
.attr("dy", 2) | |
.attr("font-size", "1rem") | |
.attr("fill", "black") | |
.attr("text-anchor", "start") | |
.style("alignment-baseline", "middle") | |
.text(function(d) { | |
switch(d){ | |
case 0: | |
return "Лучше" | |
break; | |
default: | |
return "Хуже" | |
break; | |
} | |
}); | |
}, | |
buildSkeleton:function(){ | |
this.vis['background'].append('rect') | |
.attr('rx', 6) | |
.attr('ry', 6) | |
.attr('x', this.vis.headerRect.x) | |
.attr('y', this.vis.headerRect.y) | |
.attr('width', this.vis.headerRect.width) | |
.attr('height', this.vis.headerRect.height + this.vis.mainRect.height) | |
.attr('fill', 'white') | |
.style('stroke-width', '1px') | |
.style('stroke', this.options.colors.background) | |
this.vis['background'].append('line') | |
.attr('x1', this.vis.mainRect.x) | |
.attr('y1', this.vis.mainRect.y) | |
.attr('x2', this.vis.mainRect.x + this.vis.mainRect.width) | |
.attr('y2', this.vis.mainRect.y) | |
.style('stroke-width', '1px') | |
.style('stroke', this.options.colors.background) | |
}, | |
buildAxes: function(){ | |
var yAxis = d3.axisLeft() | |
.ticks(5) | |
.tickSize(this.vis.mainRect.width) | |
.tickFormat(function(d){ | |
return d + "\u00B0"; | |
}) | |
.scale(this.vis.yScale); | |
this.vis.yAxis.attr("transform", "translate("+(this.vis.mainRect.x+this.vis.mainRect.width)+",0)") | |
.call(yAxis) | |
.call(function(g){ | |
g.select(".domain").remove(); | |
}) | |
.call(function(g){ | |
g.selectAll(".tick:not(:first-of-type) line") | |
.attr("stroke-opacity", 0.5) | |
.attr("stroke-dasharray", "2,2") | |
g.selectAll(".tick:first-of-type line").remove(); | |
}) | |
.call(function(g){ | |
g.selectAll(".tick text") | |
.style("text-anchor", "end") | |
.attr("dx", -6) | |
//.attr("dy", 0) | |
}) | |
}, | |
buildBars: function(){ | |
var that = this; | |
var roundRadius = 7; | |
var strokeWidth = 2; | |
// clip paths | |
this.vis.bars.selectAll("clipPath") | |
.data(this.data[this.options.groups[0]]['entries']) | |
.join("clipPath") | |
.attr("id", function(d){ return that.id+"-clip-"+d[0]}) | |
.append("path") | |
.attr("d", function(d){ | |
var onEndValue = that.data[that.options.groups[1]][d[0]]; | |
var worse = false; | |
if ((onEndValue - d[1]) < 0){ | |
worse = true; | |
} | |
var x = that.vis.xScale(d[0]) + strokeWidth/2; | |
var y = that.vis.yScale(d[1]) + strokeWidth/2; | |
var width = that.vis.xScale.bandwidth() - strokeWidth; | |
var height = (that.vis.mainRect.y + that.vis.mainRect.height -1) - that.vis.yScale(d[1]); | |
if(worse){ | |
return that.fullRoundedRect(x, y, width, height, roundRadius - strokeWidth/2) | |
} | |
return that.bottomRoundedRect(x, y, width, height, roundRadius) | |
}); | |
// start bars | |
this.vis.bars.selectAll("path.on-start") | |
.data(this.data[this.options.groups[0]]['entries']) | |
.join("path") | |
.classed("on-start", true) | |
.style("fill", "white") | |
.style('stroke', 'black') | |
.style('stroke-width', strokeWidth) | |
.attr("d", function(d){ | |
var onEndValue = that.data[that.options.groups[1]][d[0]]; | |
var worse = false; | |
if ((onEndValue - d[1]) < 0){ | |
worse = true; | |
} | |
var x = that.vis.xScale(d[0]); | |
var y = that.vis.yScale(d[1]); | |
var width = that.vis.xScale.bandwidth() | |
var height = (that.vis.mainRect.y + that.vis.mainRect.height -1) - that.vis.yScale(d[1]); | |
if(worse){ | |
return that.fullRoundedRect(x, y, width, height, roundRadius) | |
} | |
return that.bottomRoundedRect(x, y, width, height, roundRadius) | |
}); | |
// end bars | |
this.vis.bars.selectAll("path.on-end") | |
.data(this.data[this.options.groups[1]]['entries']) | |
.join("path") | |
.classed('on-end', true) | |
.style("fill", function(d){ | |
var onStartValue = that.data[that.options.groups[0]][d[0]]; | |
var worse = false; | |
if ((d[1] - onStartValue) < 0){ | |
worse = true; | |
} | |
if(worse){ | |
return that.options.colors.worse | |
} | |
return that.options.colors.better | |
}) | |
.style('stroke', function(d){ | |
var onStartValue = that.data[that.options.groups[0]][d[0]]; | |
var worse = false; | |
if ((d[1] - onStartValue) < 0){ | |
worse = true; | |
} | |
if(worse){ | |
return that.options.colors.worse | |
} | |
return that.options.colors.better | |
}) | |
.style('stroke-width', strokeWidth) | |
.attr("clip-path",function(d){ | |
var onStartValue = that.data[that.options.groups[0]][d[0]]; | |
var worse = false; | |
if ((d[1] - onStartValue) < 0){ | |
worse = true; | |
} | |
if(worse) { | |
return "url(#"+that.id+"-clip-"+d[0]+")" | |
} | |
return null; | |
}) | |
.attr("d", function(d){ | |
var onStartValue = that.data[that.options.groups[0]][d[0]]; | |
var worse = false; | |
if ((d[1] - onStartValue) < 0){ | |
worse = true; | |
} | |
var x = that.vis.xScale(d[0]); | |
var y = that.vis.yScale(d[1]); | |
var width = that.vis.xScale.bandwidth() | |
var height = that.vis.yScale(onStartValue) - that.vis.yScale(d[1]) - strokeWidth; | |
if(worse) { | |
//height = that.vis.yScale(onStartValue) - that.vis.yScale(d[1]); | |
} | |
return that.figuredRect(x, y, width*0.999, height, roundRadius) | |
}); | |
}, | |
buildUnchanged: function(){ | |
var that = this; | |
// create main vis svg | |
this.vis['svg'] = this.el | |
.append("svg") | |
.classed("svg-vis", true) | |
.attr('xmlns', 'http://www.w3.org/2000/svg') | |
.attr("viewBox", "0 0 "+this.options.viewBox[0]+" "+this.options.viewBox[1]) | |
.attr("perserveAspectRatio", "xMinYMid") | |
.on("click", function(event, d){ | |
that.deselectAllAndHide(); | |
}) | |
.append("svg:g") | |
this.vis['defs'] = this.vis['svg'] | |
.append("defs"); | |
this.vis.background = this.vis['svg'].append("svg:g") | |
.classed('background', true); | |
this.vis.legends = this.vis['svg'].append("svg:g") | |
.classed('legends', true); | |
this.vis.hand_legends = this.vis['svg'].append("svg:g") | |
.classed('hand_legends', true); | |
this.vis.yAxis = this.vis['svg'].append("svg:g") | |
.classed('y axis', true); | |
this.vis.markers = this.vis['svg'].append("svg:g") | |
.classed('markers', true); | |
this.vis.bars = this.vis['svg'].append("svg:g") | |
.classed('bars', true); | |
}, | |
figuredRect: function(x, y, width, height, radius){ | |
if(Math.abs(height) < radius) | |
radius = Math.floor(Math.abs(height) / 2); | |
var arcRadius = 6; | |
var hLength = (width - 2 * radius - 2 * arcRadius) / 2; | |
if( hLength < 0){ | |
hLength = 0; | |
arcRadius = (width - 2 * radius) / 2; | |
if(arcRadius < 0) | |
arcRadius = 0; | |
} | |
if(height < 0){ | |
return "M" + (x ) + "," + y | |
+ "h" + ((width - 2 * arcRadius) / 2) | |
+ "a" + arcRadius + "," + arcRadius + " 0 0 1 " + arcRadius + "," + arcRadius | |
+ "a" + arcRadius + "," + arcRadius + " 0 0 1 " + arcRadius + "," + -arcRadius | |
+ "h" + ((width - 2 * arcRadius) / 2) | |
+ "v" + (height) | |
+ "h" + -width | |
+ "z" | |
} | |
return "M" + (x + radius) + "," + y | |
+ "h" + (hLength) | |
+ "a" + arcRadius + "," + arcRadius + " 0 0 0 " + arcRadius + "," + -arcRadius | |
+ "a" + arcRadius + "," + arcRadius + " 0 0 0 " + arcRadius + "," + arcRadius | |
+ "h" + (hLength) | |
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius | |
+ "v" + (height - radius) | |
+ "h" + (-width) | |
+ "v" + (radius - height) | |
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + -radius; | |
}, | |
fullRoundedRect: function(x, y, width, height, radius){ | |
return "M" + (x + radius) + "," + y | |
+ "h" + (width - 2 * radius) | |
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius | |
+ "v" + (height - 2 * radius) | |
+ "a" + radius + "," + radius + " 0 0 1 " + -radius + "," + radius | |
+ "h" + (2 * radius - width) | |
+ "a" + radius + "," + radius + " 0 0 1 " + -radius + "," + -radius | |
+ "v" + (2 * radius - height) | |
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + -radius; | |
}, | |
bottomRoundedRect: function(x, y, width, height, radius){ | |
return "M" + x + "," + y | |
+ "h" + width | |
+ "v" + (height - radius) | |
+ "a" + radius + "," + radius + " 0 0 1 " + -radius + "," + radius | |
+ "h" + (2 * radius - width) | |
+ "a" + radius + "," + radius + " 0 0 1 " + -radius + "," + -radius | |
+ "z"; | |
}, | |
deselectAllAndHide: function(){ | |
}, | |
onResize: function (){ | |
this.deselectAllAndHide(); | |
// this.updateSvgWidthAndHeight(); | |
}, | |
updateSvgWidthAndHeight: function (){ | |
var chartElContainer = d3.select(this.options.selector); | |
var chartEl = d3.select(this.options.selector + " > svg"); | |
var chartContainerBounding = chartElContainer.node().getBoundingClientRect(); | |
var targetWidth = chartContainerBounding.width; | |
chartEl.attr("width", targetWidth); | |
chartEl.attr("height", Math.round(targetWidth / this.aspect)); | |
}, | |
}; | |
var id1_data = { | |
"hand": "left", | |
"onPeriodStart": { | |
"pointing": 72.13, | |
"middle": 75.3, | |
"ring": 70.5, | |
"pinky": 76.9 | |
}, | |
"onPeriodEnd": { | |
"pointing": 79.9, | |
"middle": 83.2, | |
"ring": 72.5, | |
"pinky": 65.6 | |
} | |
}; | |
var chart_left = new CHART_1_class('id1', '#chart'); | |
chart_left.setData(id1_data); | |
chart_left.buildVis(); | |
var id2_data = { | |
"hand": "right", | |
"onPeriodStart": { | |
"pointing": 72.13, | |
"middle": 75.3, | |
"ring": 70.5, | |
"pinky": 76.9 | |
}, | |
"onPeriodEnd": { | |
"pointing": 79.9, | |
"middle": 83.2, | |
"ring": 80.5, | |
"pinky": 75.6 | |
} | |
}; | |
var chart_right = new CHART_1_class('id2', '#chart_right'); | |
chart_right.setData(id2_data); | |
chart_right.buildVis(); |
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
#chart { | |
width: 45%; | |
background-color: #ffffff; | |
} | |
#chart_right { | |
width: 40%; | |
background-color: #ffffff; | |
} | |
.noselect { | |
-webkit-touch-callout: none; /* iOS Safari */ | |
-webkit-user-select: none; /* Safari */ | |
-khtml-user-select: none; /* Konqueror HTML */ | |
-moz-user-select: none; /* Old versions of Firefox */ | |
-ms-user-select: none; /* Internet Explorer/Edge */ | |
user-select: none; /* Non-prefixed version, currently | |
supported by Chrome, Edge, Opera and Firefox */ | |
} | |
.y.axis line{ | |
stroke: #d7e5ec; | |
} | |
.y.axis path{ | |
stroke: #d7e5ec; | |
} | |
.y.axis text{ | |
fill: #487e98; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment