/* NOTE: This code has the assumption that either the chart being produced with have a dendrogram and labels (e.g. next to the cells in a heat map) OR that the chart will have visible axes and labels along the axes. Thus in the config if config.percentElementsTake.axes.x != 0 then config.percentElementsTake.dendogram.x = 0 and vis versa */ /* A prototype function to get the absolute position of an element in the svg regardless of its relative window. This is used for the lasso tool. */ d3.selection.prototype.absolutePosition = function() { var el = this.node(); var elPos = el.getBoundingClientRect(); var vpPos = getVpPos(el); function getVpPos(el) { if(el.parentElement.tagName === 'svg') { return el.parentElement.getBoundingClientRect(); } return getVpPos(el.parentElement); } return { top: elPos.top - vpPos.top, left: elPos.left - vpPos.left, width: elPos.width, bottom: elPos.bottom - vpPos.top, height: elPos.height, right: elPos.right - vpPos.left }; }; /* A hard-coded self-made lasso icon to stand place as a temporary improvement over an empty square. */ lassoIconSvg = '\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ' // For matter of clarity we need to establish a naming convention for what labels // are horizontally oriented but are vertically stacked (labels on the y axis) // and which are vertically (or slanted) oriented but are horizontally spaced (x axis). // Let the former be y axis labels and the latter x axis labels barConfig = { svg: { id: "barchartSVG", width: 500, height: 500 }, percentElementsTake : { axes: { x: 0.1, y: 0.1 }, buttons: 0.0, dendogram: { x: 0.0, y: 0.0 }, labels: { x: 0.1, y: 0.1 }, legend: 0.0, title: 0.05, toolbar: 0.0, tooltip: 0.12, spaceBetween: { x: 0.02, y: 0.02 } } } // This is the config for a single heatmap config = { svg: { id: "heatmapSVG", width: 500, height: 500 }, percentElementsTake : { axes: { x: 0.0, y: 0.0 }, buttons: 0.0, dendogram: { x: 0.1, y: 0.1 }, labels: { x: 0.1, y: 0.1 }, legend: 0.1, title: 0.05, toolbar: 0.05, tooltip: 0.12, spaceBetween: { x: 0.02, y: 0.02 } }, linkedSVGS: [ function( subdata ) { makeLinkedBarChart(barConfig, subdata, 'meta', "sex") } ] } // global variables of some use DEBUG = true; LASSO = false; function configurateHeatmap(config) { // select svg svg = d3.select("svg#"+config.svg.id) if (svg.empty()) { if (DEBUG) { console.log("ERROR\n\tconfigurateHeatmap(config): svg selection is empty") } } svg.style("width", config.svg.width) svg.style("height", config.svg.height) } function configurateSVG(config) { // select svg svg = d3.select("svg#"+config.svg.id) if (svg.empty()) { if (DEBUG) { console.log("ERROR\n\tconfigurateHeatmap(config): svg selection is empty") } } svg.style("width", config.svg.width) svg.style("height", config.svg.height) } // get the heatmap labels acording to the keys in which they are stored function getHeatmapLabels(data, xAxisKey, yAxisKey) { // store unique labels for each access xAxisLabels = [] yAxisLabels = [] data.map( function( element ) { curYKey = element[yAxisKey] curXKey = element[xAxisKey] if ( yAxisLabels.indexOf(curYKey) == -1 ) { yAxisLabels.push( curYKey ) } if ( xAxisLabels.indexOf(curXKey) == -1 ) { xAxisLabels.push( curXKey ) } } ) return {"xAxis": xAxisLabels, "yAxis": yAxisLabels} } function getUnique(array) { // store unique labels for each access unique = [] array.map( function( element ){ if ( unique.indexOf(element) == -1 ) { unique.push( element ) } }) return unique } // number of cells wide x number of cells high function getheatmapCellDimensions(xAxisLabels, yAxisLabels) { return {x: xAxisLabels.length, y: yAxisLabels.length} } function getMetaData(data, metaKey, metaSubkey) { metaData = [] for (var i = 0; i < data.length; i++) { metaData.push(data[i][metaKey][metaSubkey]) } return metaData } function tally( array ) { tallies = {} array.map( function ( element ) { if ( d3.keys(tallies).indexOf(element) == -1 ) { tallies[element] = 1 } else { tallies[element] += 1 } }) return tallies } //-------------------------------------------------------------------// // // // MAKE AXES // // // //-------------------------------------------------------------------// // Makes axes / updates axes if they already exist function makeBarChartAxes(svg, xAxesLabels, yMax, pixelsRequiredByOthers, drawingSpace) { // y-axis var yAxisScale = d3.scaleLinear().domain([0, yMax]).range([drawingSpace.y, 0]) var yAxis = d3.axisLeft().scale(yAxisScale).tickSize(pixelsRequiredByOthers.axes.x / 4).ticks(5) // x-axis var xAxisScale = d3.scaleBand() .domain(xAxesLabels.map(function(d) { // if (d[hyperParameters.data.x].length > hyperParameters.fonts.axes.maxCharacters.x) { // return d[hyperParameters.data.x].slice(0, hyperParameters.fonts.axes.maxCharacters.x - 3) + "..." // } // for truncating long text return d.slice(0, 12) })) .range([0, drawingSpace.x]) .align([0.5]) var xAxis = d3.axisBottom().scale(xAxisScale) // y axis svg.select(".yAxisContainer").transition().duration(500).call(yAxis) svg.selectAll(".yAxisContainer").transition().duration(500).attr("transform", function(d,i){ x = pixelsRequiredByOthers.spaceBetween.y * 3 + pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.axes.y y = pixelsRequiredByOthers.spaceBetween.x * 4 + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.buttons trans = "translate("+(x)+","+(y)+")" return trans }) // x axis svg.select(".xAxisContainer").transition().duration(500).call(xAxis) svg.selectAll(".xAxisContainer").transition().duration(500).attr("transform", function(d,i){ x = pixelsRequiredByOthers.spaceBetween.y * 4 + pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.axes.y y = pixelsRequiredByOthers.spaceBetween.x * 5 + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.buttons + drawingSpace.y trans = "translate("+(x)+","+(y)+")" return trans }) // Adjust text to angle, fontsize and fontfamily xFontSize = pixelsRequiredByOthers.labels.x / 4 yFontSize = pixelsRequiredByOthers.labels.y / 4 svg.selectAll(".xAxisContainer") .selectAll('text') .attr("text-anchor", "end") .attr("font-size", xFontSize) .attr("transform", "rotate(-45)").transition().duration(500) .attr("x", -xFontSize) .attr("y", xFontSize) svg.selectAll(".yAxisContainer") .selectAll('text') .attr("font-size", yFontSize) } //-------------------------------------------------------------------// // // // MAKE LASSO // // // //-------------------------------------------------------------------// /* Lasso function call heirarchy: setUpLasso - calls and organizes the following | setUpLassoIcon (0) | | lassoCells (1) | | | |---> lassoTick (2) | |-----| ---> checkWhichCellsInLasso(svg, lassoPointData) (3) | |-----| ---> applyToCellsInLasso(svg) (4) function 0 sets up the toggle indecator and global boolean toggle to the lasso icon function 1 creates the container for the the lasso polygon which the user sees function 2 is called by function 1 and it adds the current mouse position to lasso and then calls functions 3 and 4 function 3 marks which cells of the heatmap are inside the lasso with the class inLasso function 4 finally inacts what should happen to cells in the selection Currently, changes selected cell styles and then calls linked svg functions with the subset data Also, it resets non-select cells to default styles. */ function setUpLasso(data, config, svg, chartContainer, toolbarContainer, pixelsRequiredByOthers) { setUpLassoIcon(data, svg, toolbarContainer, pixelsRequiredByOthers) // stores the mouse movements var lassoPointData = []; svg.on("mousedown", function(d, i){ lassoCells(svg, chartContainer, config) }) svg.on("mouseup", function(d, i) { // remove the polygon svg.select(".lassoContainer").remove(); // stop tracking mouse movements svg.on("mousemove", null); }) } function setUpLassoIcon(data, svg, toolbarContainer, pixelsRequiredByOthers) { /* This will make the lassoIcon and add the event listener for when the lassoIcon is clicked */ if (toolbarContainer.select(".lassoIconContainer").empty()) { lassoIconContainer = toolbarContainer.append("g").attr("class","lassoIconContainer") // move lasso over } else { lassoIconContainer = toolbarContainer.select("g.lassoIconContainer") } // put the hardcoded image in there if (lassoIconContainer.select("svg").empty()) { lassoIconSVGContainer = lassoIconContainer.append("svg") } lassoIconSVGContainer = lassoIconContainer.select("svg") lassoIconSVGContainer.node().innerHTML = lassoIconSvg // add an invisible rectangle as otherwise mousedown doesnt register if (lassoIconContainer.select("rect.hiddenBox").empty()) { lassoIconContainer.append("rect").attr("class", 'hiddenBox') } lassoIconContainer.select("rect.hiddenBox") .attr("width", pixelsRequiredByOthers.toolbar) .attr("height", pixelsRequiredByOthers.toolbar) .attr("fill", "white").style("opacity",0) .on("mousedown", lassoIconMousedown) function lassoIconMousedown(d, i) { if (LASSO) { LASSO = false // toggle lasso ability on and off lassoIconSVGContainer.selectAll("circle") .style("fill", "white") // inside of lasso is white // when it goes off, reset all cells to default style svg.selectAll(".cellContainer").selectAll("rect") .attr("stroke-width", 1) .style("opacity", 1) // remove lasso class from all cells svg.selectAll(".cellContainer").classed("inLasso", false); for (var i = 0; i < config.linkedSVGS.length; i++) { config.linkedSVGS[i](data) } } else { LASSO = true // color the icon to give user notification lassoIconSVGContainer.selectAll("circle") .style("fill", "cyan") } } } // track the cells in the lasso function lassoCells(svg, chartContainer, config) { if (!LASSO) {return null} // only proceed if Lasso is turned on // store the points where the mouse was at lassoPointData = []; // function for making the polygon var lassoLine = d3.line().x(function(d, i) { return d[0]; }).y(function(d, i) { return d[1]; }); // container for the lasso lassoContainer = chartContainer.append("g").attr("class", "lassoContainer") // stylize the lasso lasso = lassoContainer.append("path") // dont worry about the append because .data([lassoPointData]) // the container will be removed on mouse up .attr("class", "line") .attr("d", lassoLine) .attr("fill", "blue") .style("opacity", 0.3) .attr("stroke", "blue") .attr("stroke-width", 3) // on move, append another point svg.on("mousemove", function() { var pt = d3.mouse(this); lassoTick(pt, lassoPointData, lasso, lassoLine, svg, config); }); } // end lassoCells function checkWhichCellsInLasso(svg, lassoPointData) { // for each cell of the heatmap svg.selectAll(".cellContainer").each(function(d, i) { // get current cell of the heatmap currentCell = d3.select(this) // get absolute position of this cell in the svg currentBox = currentCell.absolutePosition() // get the four corners of the bounding box pts = [ [currentBox.left, currentBox.top], [currentBox.right, currentBox.top], [currentBox.left, currentBox.bottom], [currentBox.right, currentBox.bottom] ] // helper function to see if all the points are in the lasso function allPointsInLassoHullQ(element, index, array) { return d3.polygonContains(lassoPointData, element) } // boolean using above function allPointsInLasso = pts.every(allPointsInLassoHullQ) // if the entire cell is inside the lasso, change the class and style if (allPointsInLasso) { currentCell.classed("inLasso", true); } else { currentCell.classed("inLasso", false); } }) // end each } function applyToCellsInLasso(svg, config) { subsetData = [] svg.selectAll(".cellContainer").each(function(){ currentCell = d3.select(this) if (currentCell.classed("inLasso")) { currentCell.select("rect").attr("stroke-width", 3) currentCell.raise() subsetData.push(currentCell.data()[0]) } else { currentCell.select("rect") .attr("stroke-width", 1) .style("opacity", 1) currentCell.classed("inLasso", false); } }) for (var i = 0; i < config.linkedSVGS.length; i++) { config.linkedSVGS[i](subsetData) } } // when a new point is pushed function lassoTick(pt, lassoPointData, lasso, lassoLine, svg, config) { // push a new data point onto the back lassoPointData.push(pt); // Redraw the path: lasso.attr("d", function(d) { return lassoLine(d);}) // make the hull of the lasso and test to see what is inside if more than // 3 points if (lassoPointData.length < 3) { return null } checkWhichCellsInLasso(svg, lassoPointData) applyToCellsInLasso(svg, config) } // end lassoTick function getChartGroups(svg) { if (svg.selectAll("g").empty()) { /****************************************************************** * * * set up groups * * * ******************************************************************/ // contains all chart elements, e.g. labels, dendogram, buttons, etc var chartContainer = svg.append("g").attr("class", "chartContainer") // contains the cells for the heat map var plotContainer = chartContainer.append("g").attr("class", "plotContainer") // container for the labels along the axes var axesLabelContainer = chartContainer.append("g").attr("class", "axesLabelContainer") var xAxisLabelContainer = axesLabelContainer.append("g").attr("class", "xAxisLabelContainer") var yAxisLabelContainer = axesLabelContainer.append("g").attr("class", "yAxisLabelContainer") // container for the axes themselves var axesContainer = chartContainer.append("g").attr("class", "axesContainer") var xAxisContainer = axesContainer.append("g").attr("class", "xAxisContainer") var yAxisContainer = axesContainer.append("g").attr("class", "yAxisContainer") // Title container var titleContainer = chartContainer.append("g").attr("class", "titleContainer") // svg button container (for toggling between data) var buttonContainer = chartContainer.append("g").attr("class", "buttonContainer") // svg button container (for toggling between data) var toolbarContainer = chartContainer.append("g").attr("class", "toolbarContainer") // color legend var legendContainer = chartContainer.append("g").attr("class", "legendContainer") // container for the dendograms along the axes var dendogramContainer = chartContainer.append("g").attr("class", "dendogramContainer") var xAxisdendogramContainer = dendogramContainer.append("g").attr("class", "xAxisdendogramContainer") var yAxisdendogramContainer = dendogramContainer.append("g").attr("class", "yAxisdendogramContainer") } else { /****************************************************************** * * * select groups * * * ******************************************************************/ // contains all chart elements, e.g. labels, dendogram, buttons, etc var chartContainer = svg.select("g.chartContainer") // contains the cells for the heat map var plotContainer = chartContainer.select("g.plotContainer") // container for the labels along the axes var axesLabelContainer = chartContainer.select("g.axesLabelContainer") var xAxisLabelContainer = axesLabelContainer.select("g.xAxisLabelContainer") var yAxisLabelContainer = axesLabelContainer.select("g.yAxisLabelContainer") // container for the axes themselves var axesContainer = chartContainer.select("g.axesContainer") var xAxisContainer = axesContainer.select("g.xAxisContainer") var yAxisContainer = axesContainer.select("g.yAxisContainer") // Title container var titleContainer = chartContainer.select("g.titleContainer") // svg button container (for toggling between data) var buttonContainer = chartContainer.select("g.buttonContainer") // svg button container (for toggling between data) var toolbarContainer = chartContainer.select("g.toolbarContainer") // color legend var legendContainer = chartContainer.select("g.legendContainer") // container for the dendograms along the axes var dendogramContainer = chartContainer.select("g.dendogramContainer") var xAxisdendogramContainer = dendogramContainer.select("g.xAxisdendogramContainer") var yAxisdendogramContainer = dendogramContainer.select("g.yAxisdendogramContainer") } // return the selections return [ chartContainer, plotContainer, axesLabelContainer, xAxisContainer, yAxisContainer, axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer, titleContainer, toolbarContainer, buttonContainer, legendContainer, dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer ] } function moveChartGroups(config, drawingSpace, pixelsRequiredByOthers, chartContainer, plotContainer, axesLabelContainer, xAxisContainer, yAxisContainer, axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer, titleContainer, toolbarContainer, buttonContainer, legendContainer, dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer ) { plotContainer.attr("transform",function(d, i) { x = (pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.axes.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.spaceBetween.y * 4) // pixelsRequiredByOthers.spaceBetween.y * 3 + pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.axes.y y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.x * 4) trans = "translate("+(x)+","+(y)+")" return trans }) titleContainer.attr("transform", function(d, i) { x = parseFloat(svg.style("width")) / 2 y = (pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.spaceBetween.y * 2) trans = "translate("+(x)+","+(y)+")" return trans }) legendContainer.attr("transform", function(d, i) { x = (pixelsRequiredByOthers.spaceBetween.x * 1) y = (pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.spaceBetween.y * 3) trans = "translate("+(x)+","+(y)+")" return trans }) // move it over yAxisdendogramContainer.attr("transform",function(){ x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 3) y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.spaceBetween.y * 6 + drawingSpace.y + pixelsRequiredByOthers.dendogram.y) xCent = (x + drawingSpace.x / 2) yCent = (y + pixelsRequiredByOthers.dendogram.y / 2) trans = "translate("+(x)+","+(y)+")" // rot = "rotate(90 "+(xCent)+" "+(yCent)+")"; return trans }) xAxisdendogramContainer.attr("transform",function(){ x = (pixelsRequiredByOthers.spaceBetween.x * 1) y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 4) xCent = (x + drawingSpace.x / 2) yCent = (y + pixelsRequiredByOthers.dendogram.y / 2) trans = "translate("+(x)+","+(y)+")" // rot = "rotate(90 "+(xCent)+" "+(yCent)+")"; return trans }) legendContainer.attr("transform", function(d, i) { x = (pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + drawingSpace.x + pixelsRequiredByOthers.spaceBetween.y * 5) y = (pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.spaceBetween.y * 4) trans = "translate("+(x)+","+(y)+")" return trans }) toolbarContainer.attr("transform", function(d, i) { x = (pixelsRequiredByOthers.spaceBetween.x * 1) y = (pixelsRequiredByOthers.spaceBetween.y * 1) trans = "translate("+(x)+","+(y)+")" return trans }) } function makeLinkedBarChart(config, data, metaKey, metaSubkey) { configurateSVG(config) // select the svg var svg = d3.select("svg#"+config.svg.id) // extract the relevant meta data var metaData = getMetaData(data, metaKey, metaSubkey) var tallies = tally(metaData) var labels = d3.keys(tallies) // temp title element for demo purposes var title = "My Linked Barchart" // calculate space needed for all elements var pixelsRequiredByOthers = calculatePixelsNeeded(config.svg.id, config.percentElementsTake) var drawingSpace = getDrawingSpace(config.svg.id, pixelsRequiredByOthers) // get min / max of data var dataExtent = { min: d3.min(labels.map(function(d){return tallies[d]})), max: d3.max(labels.map(function(d){return tallies[d]})) } /****************************************************************** * get main groups * ******************************************************************/ var [ chartContainer, plotContainer, axesLabelContainer, xAxisContainer, yAxisContainer, axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer, titleContainer, toolbarContainer, buttonContainer, legendContainer, dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer ] = getChartGroups(svg) // makes / gets groups // move groups to their respective positions within the chart moveChartGroups(config, drawingSpace, pixelsRequiredByOthers, chartContainer, plotContainer, axesLabelContainer, xAxisContainer, yAxisContainer, axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer, titleContainer, toolbarContainer, buttonContainer, legendContainer, dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer ) // parameters for the bars in this bar chart var bar = { width: drawingSpace.x / (labels.length + 1), // one bar's width will be used for the spacers between the bars spacer: (drawingSpace.x / (labels.length + 1)) / (labels.length + 1), // in total there will be (numberOfBars + 1) spacers scale: d3.scaleLinear() .domain([0, dataExtent.max]) .range([0,drawingSpace.y]), color: d3.scaleLinear() .domain([dataExtent.min, dataExtent.max]) .range(["cyan", "blue"]) } /****************************************************************** * * * set up barchart proper * * * ******************************************************************/ // current number of bars numberOfBars = plotContainer.selectAll(".barContainer").size() // number of bars needed numberOfBarsNeeded = labels.length if (numberOfBarsNeeded > numberOfBars) { // need more bars plotContainer.selectAll(".barContainer") .data(labels) // bind data .enter() // will only produce (numberOfBarsNeeded - numberOfBars) # of bars .append("g").attr("class", "barContainer") .append("rect") .attr("stroke", "black") // all bars will have the following styles .attr("stroke-width", "1px") // as they will originally be created here .attr("rx", "10px") .attr("ry", "10px") } else { // remove excess bars plotContainer.selectAll(".barContainer") .data(labels) // bind data .exit().remove() // remove excess } // adjust things for pre-existing bars plotContainer.selectAll(".barContainer") .attr("transform", function(d, i) { x = i * bar.width + bar.spacer * (i+1) // + 1 for the leading spacer y = drawingSpace.y - bar.scale(tallies[d]) trans = "translate("+(x)+","+(y)+")" return trans }) // move bars to respective position .select("rect") .attr("width", bar.width) .attr("height", function (d, i) { return bar.scale(tallies[d]) }) // reset the height of all bars .attr("fill", function (d, i) { return bar.color(tallies[d]) }) // bar cell .on("mousemove", mousemoveBar) .on("mouseout", mouseoutBar) /****************************************************************** * set up title * ******************************************************************/ makeTitle(svg, titleContainer, pixelsRequiredByOthers, title) makeBarChartAxes(svg, labels, dataExtent.max, pixelsRequiredByOthers, drawingSpace) function mousemoveBar(d, i) { // simple tooltip for demo purposes var tooltip = svg.select("g.tooltip") if (tooltip.empty()) { tooltip = svg.append("g").attr("class","tooltip") } // move tooltip to mouse location tooltip.attr("transform", function(d, i) { x = d3.event.pageX - document.getElementById(config.svg.id).getBoundingClientRect().x + 10 y = d3.event.pageY - document.getElementById(config.svg.id).getBoundingClientRect().y + 10 trans = "translate("+(x)+","+(y)+")" return trans }) // add a background for the tooltip rather than use an external div tooltipRect = tooltip.append("rect").attr("fill", "white") // add the text tooltipText = tooltip.append("g").attr("class","tooltipText") tooltipText.append("text").text(tallies[d]) .attr("text-anchor","middle") tooltipText.attr("transform","translate("+(tooltipText.node().getBBox().width)+","+(tooltipText.node().getBBox().height)+")") // stylize the rectangle tooltipRect.attr("fill", "white") .attr("width", tooltipText.node().getBBox().width * 2) .attr("height", tooltipText.node().getBBox().height * 2) .attr("rx", 10) .attr("ry", 10) .attr("stroke","black") tooltip.raise() } function mouseoutBar(d, i) { svg.selectAll("g.tooltip").remove() } } function makeTitle(svg, titleContainer, pixelsRequiredByOthers, title) { if ( titleContainer.select("text").empty() ) { titleContainer.append("text") } titleContainer.select("text").text(title) // move the title to position .attr('text-anchor',"middle") // lazy centering .attr("font-size", pixelsRequiredByOthers.title + "px") .style("user-select", "none") // disable user select as it interfers with lasso .style("pointer-events", "none") } // A sloppy temp for the make heatmap function function makeHeatmap(config, data, labelKeys) { // select the svg var svg = d3.select("svg#"+config.svg.id) // start to fill the heatmap var labels = getHeatmapLabels(data, labelKeys.x, labelKeys.y) // a temp title title to see spacing var title = "My Heatmap" // The pixel space used up by all other elements besides the actual cells of // the heatmap var pixelsRequiredByOthers = calculatePixelsNeeded(config.svg.id, config.percentElementsTake) // the pixel space required by the actual heatmap var drawingSpace = getDrawingSpace(config.svg.id, pixelsRequiredByOthers) // min / max values of the cells var dataExtent = { min: d3.min(data.map(function(d){return d.val;})), max: d3.max(data.map(function(d){return d.val;})) } // number of cells wide x number of cells high cellDimensions = getheatmapCellDimensions(labels.xAxis, labels.yAxis) // the size of a single cell in the heatmap cellSize = {x: drawingSpace.x / cellDimensions.x, y: drawingSpace.y / cellDimensions.y} // the color interpolation function cellColors = d3.scaleSequential(d3.interpolateGnBu).domain([dataExtent.min, dataExtent.max]) /****************************************************************** * get main groups * ******************************************************************/ var [ chartContainer, plotContainer, axesLabelContainer, xAxisContainer, yAxisContainer, axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer, titleContainer, toolbarContainer, buttonContainer, legendContainer, dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer ] = getChartGroups(svg) // makes / gets groups // move groups to their respective positions within the chart moveChartGroups(config, drawingSpace, pixelsRequiredByOthers, chartContainer, plotContainer, axesLabelContainer, xAxisContainer, yAxisContainer, axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer, titleContainer, toolbarContainer, buttonContainer, legendContainer, dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer ) /****************************************************************** * * * set up heatmap proper * * * ******************************************************************/ // current number of cells numberOfCells = plotContainer.selectAll(".cellContainer").size() // number of cells needed numberOfCellsNeeded = cellDimensions.x * cellDimensions.y if (numberOfBarsNeeded > numberOfCells) { plotContainer.selectAll(".cellContainer") .data(data) // bind data .enter() .append("g").attr("class", "cellContainer") .append("rect") .attr("stroke", "black") .attr("stroke-width", "1px") .attr("rx", "10px") .attr("ry", "10px") } else { plotContainer.selectAll(".cellContainer") .data(data) // bind data .exit().remove() // remove extra containers } plotContainer.selectAll(".cellContainer") .attr("transform", function(d, i) { xAxislabelIndex = labels.xAxis.indexOf(d[labelKeys.x]); yAxislabelIndex = labels.yAxis.indexOf(d[labelKeys.y]); x = cellSize.x * xAxislabelIndex; y = cellSize.y * yAxislabelIndex; trans = "translate("+(x)+","+(y)+")" return trans }) // move cells to respective position .select("rect") .attr("width", cellSize.x) .attr("height", cellSize.y) .attr("fill", function(d, i) {return cellColors(d.val)}) // color cell .on("mousemove", mousemoveCell) // add tooltip .on("mouseout", mouseoutCell) // remove tooltip function mousemoveCell(d, i) { // simple tooltip for demo purposes tooltip = d3.select("g.tooltip") if (tooltip.empty()) { tooltip = svg.append("g").attr("class","tooltip") } // move tooltip to mose location tooltip.attr("transform", function(d, i) { x = d3.event.pageX - document.getElementById(config.svg.id).getBoundingClientRect().x + 10 y = d3.event.pageY - document.getElementById(config.svg.id).getBoundingClientRect().y + 10 trans = "translate("+(x)+","+(y)+")" return trans }) // add a background for the tooltip rather than use an external div tooltipRect = tooltip.append("rect").attr("fill", "white") // add the text tooltipText = tooltip.append("g").attr("class",".tooltipText") tooltipText.append("text").text(d.val.toFixed(4)) .attr("text-anchor","middle") tooltipText.attr("transform","translate("+(tooltipText.node().getBBox().width)+","+(tooltipText.node().getBBox().height)+")") .style("user-select", "none") .style("pointer-events", "none") // stylize the rectangle tooltipRect.attr("fill", "white") .attr("width", tooltipText.node().getBBox().width * 2) .attr("height", tooltipText.node().getBBox().height * 2) .attr("rx", 10) .attr("ry", 10) .attr("stroke","black") } function mouseoutCell(d, i) { svg.selectAll("g.tooltip").remove() } /****************************************************************** * set up title * ******************************************************************/ makeTitle(svg, titleContainer, pixelsRequiredByOthers, title) /****************************************************************** * set up LASSO * ******************************************************************/ setUpLasso(data, config, svg, chartContainer, toolbarContainer, pixelsRequiredByOthers) /****************************************************************** * * * set up colored legend * * * ******************************************************************/ addGnBuVerticalGradient(svg) if ( legendContainer.select("rect.colorRect") ) { colorLegendRect = legendContainer.append("rect").attr("class", "colorRect") } if ( legendContainer.select("text.legendMin") ) { legendContainer.append("text").attr("class", "legendMin") } if ( legendContainer.select("text.legendMax") ) { legendContainer.append("text").attr("class", "legendMax") } // the colored rectangle colorLegendRect = legendContainer.select("rect") .attr("width", pixelsRequiredByOthers.legend) .attr("height", drawingSpace.y * 0.8) .attr("rx", 10) .attr("ry", 10) .attr("transform", "translate(0,"+(drawingSpace.y * 0.1)+")") .style("fill", "url(#GnBuVerticalGradient)") .attr("stroke","black") // text for min legendContainer.select("text.legendMin") .attr("transform", "translate("+(pixelsRequiredByOthers.legend * 0.5)+","+(drawingSpace.y * 0.1 - 6)+")") .attr("class","legendText").text(dataExtent.min.toFixed(4)) .attr("font-size", 12) .attr("text-anchor", "middle") .style("user-select", "none") // no user interaction with text .style("pointer-events", "none") // text for max legendContainer.select("text.legendMax") .attr("transform", "translate("+(pixelsRequiredByOthers.legend * 0.5)+","+(drawingSpace.y * .9 + 12)+")") .attr("class","legendText").text(dataExtent.max.toFixed(4)) .attr("font-size", 12) .attr("text-anchor", "middle") .style("user-select", "none") .style("pointer-events", "none") /****************************************************************** * * * set up labels * * * ******************************************************************/ xLabels = xAxisLabelContainer.selectAll(".xLabel").data(labels.xAxis).enter() .append("g").attr("class","xLabel") .attr("transform", function(e, i) { x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 3) y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 5) x += cellSize.x * i + cellSize.x / 2; y += cellSize.y * cellDimensions.y + pixelsRequiredByOthers.labels.y / 2 ; trans = "translate("+(x)+","+(y)+")"; return trans }) // move the labels over xLabels.append("text").text(function(d){return d}) .style("user-select", "none") .style("pointer-events", "none") // no user interaction with the text yLabels = xAxisLabelContainer.selectAll(".yLabel").data(labels.yAxis).enter() .append("g").attr("class","yLabel") .attr("transform", function(e, i) { x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 2) y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 4) x += -pixelsRequiredByOthers.labels.x / 2 y += cellSize.y * i + cellSize.y / 2; trans = "translate("+(x)+","+(y)+")"; return trans }) // move the labels over yLabels.append("text").text(function(d){return d}) .style("user-select", "none") .style("pointer-events", "none") // no user interaction with the text /****************************************************************** * * * set up dendogram * * * ******************************************************************/ var yRoot = d3.stratify() .id(function(d) { return d.name; }) .parentId(function(d) { return d.parent; }) (yDend); // make the data heirarchical // make the tree var yDendoTree = d3.tree().size([drawingSpace.x, pixelsRequiredByOthers.dendogram.y]); // make the links var yLinks = yAxisdendogramContainer.selectAll(".link") .data(yDendoTree(yRoot).links()) .enter().append("path") .attr("class", "link") .attr("d", d3.linkVertical() .x(function(d) { return d.x; }) .y(function(d) { return -d.y; })) .attr("fill", "none") .attr("stroke", "black") // move the tree yAxisdendogramContainer.attr("transform",function(){ x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 3) y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.spaceBetween.y * 6 + drawingSpace.y + pixelsRequiredByOthers.dendogram.y) xCent = (x + drawingSpace.x / 2) yCent = (y + pixelsRequiredByOthers.dendogram.y / 2) trans = "translate("+(x)+","+(y)+")" // rot = "rotate(90 "+(xCent)+" "+(yCent)+")"; return trans }) // hierarchical var xRoot = d3.stratify() .id(function(d) { return d.name; }) .parentId(function(d) { return d.parent; }) (xDend); // make the tree var xDendoTree = d3.tree().size([drawingSpace.y,pixelsRequiredByOthers.dendogram.x]); // make the links var xLinks = xAxisdendogramContainer.selectAll(".link") .data(xDendoTree(xRoot).links()) .enter().append("path") .attr("class", "link") .attr("d", d3.linkHorizontal() .x(function(d) { return d.y; }) .y(function(d) { return d.x; })) .attr("fill", "none") .attr("stroke", "black") // move the tree xAxisdendogramContainer.attr("transform",function(){ x = (pixelsRequiredByOthers.spaceBetween.x * 1) y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 4) xCent = (x + drawingSpace.x / 2) yCent = (y + pixelsRequiredByOthers.dendogram.y / 2) trans = "translate("+(x)+","+(y)+")" // rot = "rotate(90 "+(xCent)+" "+(yCent)+")"; return trans }) } function calculatePixelsNeeded(svgID, percentElementsTake) { /* this turns the percentages of the elements around the HeatMap (title, labels, dendogram, legend, etc) into pixel values the function getDrawingSpace calculates how much space is left over for the heatmap proper */ var svg = d3.select("svg#"+svgID) var w = parseFloat(svg.style("width")) var h = parseFloat(svg.style("height")) var pixelsRequired = { axes: { x: percentElementsTake.axes.x * w, y: percentElementsTake.axes.y * h }, dendogram: { x: percentElementsTake.dendogram.x * w, y: percentElementsTake.dendogram.y * h }, labels: { x: percentElementsTake.labels.x * w, y: percentElementsTake.labels.y * h, }, title: percentElementsTake.title * h, toolbar: percentElementsTake.toolbar * h, buttons: percentElementsTake.buttons * h, legend: percentElementsTake.legend * w, spaceBetween: { x: percentElementsTake.spaceBetween.x * w, y: percentElementsTake.spaceBetween.x * h } } return pixelsRequired } function getDrawingSpace(svgID, pixelsRequiredByOtherElements) { /* this uses the pixel values of other elements in the chart (e.g. title, labels) and calculates how much space is left over for the heatmap proper */ var svg = d3.select("svg#"+svgID) var w = parseFloat(svg.style("width")) var h = parseFloat(svg.style("height")) // for convience var margins = pixelsRequiredByOtherElements var drawingSpace = { x: w - (margins.axes.x + margins.dendogram.x + margins.labels.x + margins.legend + margins.spaceBetween.y * 6), y: h - (margins.axes.y + margins.dendogram.y + margins.labels.y + margins.title + margins.toolbar + margins.buttons + margins.spaceBetween.x * 8) } return drawingSpace } function addGnBuVerticalGradient(svg) { //Append a defs (for definition) element to your SVG if ( svg.select("defs").empty() ) { var defs = svg.append("defs"); } var defs = svg.select("defs"); //Append a linearGradient element to the defs and give it a unique id if (defs.select("#legendLinearGradient").empty()) { var legendLinearGradient = defs.append("linearGradient") .attr("id", "GnBuVerticalGradient"); } var legendLinearGradient = defs.select("linearGradient") // vertical gradient legendLinearGradient.attr("x1", "0%") .attr("y1", "0%") .attr("x2", "0%") .attr("y2", "100%"); // A color scale (same colors as interpolateGnBu) var legendColorScale = d3.scaleLinear().range(["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"]); //Append multiple color stops by binding data legendLinearGradient.selectAll("stop") .data( legendColorScale.range() ) .enter().append("stop") .attr("offset", function(d,i) { return i/(legendColorScale.range().length-1); }) .attr("stop-color", function(d) { return d; }); } configurateHeatmap(config) makeLinkedBarChart(barConfig, data, 'meta', "sex") makeHeatmap(config, data, {x:"xName",y:"yName"})