Last active
April 30, 2017 13:07
-
-
Save SumNeuron/262e37e2f932cf4b693f241c52a410ff to your computer and use it in GitHub Desktop.
D3 (v4) Flexible and Responsive Box and Whisker Chart
This file contains 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
{ | |
"dataset_a": [ | |
{"data":"a", "min": 1, "Q1": 1.5, "Q2": 2, "Q3": 4, "max":6}, | |
{"data":"b", "min": 2, "Q1": 2.2, "Q2": 2.7, "Q3": 3, "max":4}, | |
{"data":"c", "min": 5, "Q1": 5.5, "Q2": 7, "Q3": 7.3, "max":8}, | |
{"data":"d", "min": 6, "Q1": 6.5, "Q2": 7, "Q3": 8, "max":10} | |
], | |
"dataset_b": [ | |
{"data":"a", "min": 5, "Q1": 5.5, "Q2": 7, "Q3": 7.3, "max":8}, | |
{"data":"b", "min": 6, "Q1": 6.5, "Q2": 7, "Q3": 8, "max":10} | |
], | |
"dataset_c": [ | |
{"data":"a", "min": 6, "Q1": 6.5, "Q2": 7, "Q3": 8, "max":10}, | |
{"data":"b", "min": 2, "Q1": 2.2, "Q2": 2.7, "Q3": 3, "max":4}, | |
{"data":"c", "min": 5, "Q1": 5.5, "Q2": 7, "Q3": 7.3, "max":8}, | |
{"data":"d", "min": 3, "Q1": 3.2, "Q2": 4, "Q3": 6, "max":7}, | |
{"data":"e", "min": 1, "Q1": 1.5, "Q2": 2, "Q3": 4, "max":6}, | |
{"data":"f", "min": 0.2, "Q1": 1, "Q2": 3, "Q3": 4, "max":4.5} | |
] | |
} |
This file contains 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
var charts_config = ajax_json("charts_configuration.json") | |
var box_and_whiskers_data = ajax_json("box_and_whiskers.json") | |
make_box_and_whiskers_chart(box_and_whiskers_data) |
This file contains 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
{ | |
"files": { | |
"box_and_whiskers": "box_and_whiskers.json" | |
}, | |
"document_state": { | |
"box_and_whiskers": "none" | |
}, | |
"chart_ids": { | |
"box_and_whiskers": "box_and_whiskers_chart" | |
}, | |
"svg": { | |
"width": "80%", | |
"height": "80%" | |
}, | |
"plot_attributes": { | |
"title": { | |
"family": "Helvetica", | |
"size": 20 | |
}, | |
"tooltip": { | |
"curve": 5, | |
"point": 10, | |
"fill": "rgb(51, 51, 51)", | |
"stoke": "rgb(51, 51, 51)", | |
"opacity": 0.8, | |
"text": "cyan", | |
"emphasis": "white", | |
"family": "Times", | |
"size": 12, | |
"default_seperation_from_object": 2 | |
}, | |
"axes": { | |
"family": "Courier New", | |
"size": 10, | |
"ticks": { | |
"size": 5 | |
}, | |
"maxCharacters": { | |
"x": 10 | |
}, | |
"labels": { | |
"family": "Courier New", | |
"size": 14 | |
} | |
}, | |
"buttons": { | |
"family": "Courier New", | |
"size": 12, | |
"stroke": "black", | |
"fill": { | |
"not_selected": "white", | |
"selected": "black" | |
} | |
} | |
}, | |
"plots": { | |
"scatter": { | |
"point": { | |
"radius": 5 | |
}, | |
"opacity": { | |
"hover": 0.5, | |
"nonhover": 0.8 | |
} | |
}, | |
"line": { | |
"width": 5, | |
"stroke": "#2196F3", | |
"fill": "none", | |
"hidden_radius": 10, | |
"opacity": { | |
"hover": 0.5, | |
"nonhover": 0.8 | |
} | |
}, | |
"box_and_whiskers": { | |
"spacing": 2, | |
"width": 1, | |
"stroke": "black", | |
"whiskers": { | |
"width": 2 | |
}, | |
"opacity": { | |
"hover": 0.5, | |
"nonhover":1 | |
} | |
} | |
}, | |
"colors": { | |
"palette": [ | |
"#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", | |
"#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", | |
"#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", | |
"#607D8B" | |
] | |
} | |
} |
This file contains 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
/************************************************************************** | |
* * | |
* HELPER FUNCTIONS * | |
* * | |
**************************************************************************/ | |
// load a json file | |
function ajax_json(file, sync=true) { | |
var data = $.ajax({ | |
url: file, | |
async: !sync, | |
dataType: 'json', | |
success: function (data) { | |
return data | |
}, | |
error: function() { | |
console.log("File " + file + " failed to load.") | |
} | |
}); | |
return data.responseJSON | |
} | |
function keys(data) { | |
return Object.keys(data) | |
} | |
function parseNumber(number) { | |
var number_string = number.toString() | |
if (number_string.includes("%")) { | |
number_string = number_string.slice(0, number_string.length - 1) | |
if (number_string.length == 3) { | |
return 1.0 | |
} else { | |
return parseFloat("." + number_string) | |
} | |
} else if (number_string.includes(".")) { | |
return parseFloat(number_string) | |
} else { | |
return parseInt(number_string) | |
} | |
} | |
function typeofNumber(number) { | |
number = number.toString() | |
if (number.includes("%")) { | |
return "percent" | |
} else if (number.includes(".")) { | |
return "float" | |
} else if (number.includes("e") || number.includes("+")) { | |
return "scientific" | |
} else { | |
return "integer" | |
} | |
} |
This file contains 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
<!DOCTYPE html> | |
<html> | |
<head> | |
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.js"></script> | |
<script src="helpers.js" defer></script> | |
<script src="make_axes.js" defer></script> | |
<script src="make_box_and_whiskers_chart.js" defer></script> | |
<script src="make_buttons.js" defer></script> | |
<script src="make_margins.js" defer></script> | |
<script src="make_svg.js" defer></script> | |
<script src="make_title.js" defer></script> | |
<script src="make_tooltip.js" defer></script> | |
<script src="charts_configuration.js" defer></script> | |
</head> | |
<style> | |
html { | |
width: 100%; | |
height: 100%; | |
} | |
body { | |
width: 100%; | |
height: 100%; | |
} | |
section { | |
margin:auto; | |
width:100%; | |
height:100%; | |
} | |
</style> | |
<body onresize="make_box_and_whiskers_chart(box_and_whiskers_data)"> | |
<section id="box_and_whiskers_chart" class="d3_chart"> | |
</section> | |
</body> |
This file contains 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
//-------------------------------------------------------------------// | |
// // | |
// MAKE AXES // | |
// // | |
//-------------------------------------------------------------------// | |
function calculate_space_needed_by_axes(chart_group, data_extent, margins) { | |
// Temporary SVG group element to calculate size of dummy axes then be removed | |
var temp = chart_group.append("g").attr("class", "temp") | |
// Dummy scales. Use max and min to get accurate measurement for length of ticks | |
var x_scale = d3.scaleLinear().domain([data_extent.min.x, data_extent.max.x]).range([0, 1]) | |
var y_scale = d3.scaleLinear().domain([data_extent.min.y, data_extent.max.y]).range([0, 1]) | |
// Make the axes | |
var x_axis_scaled = d3.axisBottom().scale(x_scale).tickSize(charts_config.plot_attributes.axes.ticks.size).ticks(6) | |
var y_axis_scaled = d3.axisLeft().scale(y_scale).tickSize(charts_config.plot_attributes.axes.ticks.size).ticks(6) | |
// Place and maintain the axis as an SVG object | |
var x_axis= temp.append("g").attr("class", "temp").call(x_axis_scaled).attr("font-size", charts_config.plot_attributes.axes.size).attr("font-family", charts_config.plot_attributes.axes.family) | |
var y_axis= temp.append("g").attr("class", "temp").call(y_axis_scaled).attr("font-size", charts_config.plot_attributes.axes.size).attr("font-family", charts_config.plot_attributes.axes.family) | |
// Add the axes labels. y-axis is rotated | |
var x_axis_label = temp.append("text").text("temp").attr("font-family", charts_config.plot_attributes.axes.labels.family).attr("font-size", charts_config.plot_attributes.axes.labels.size) | |
var y_axis_label = temp.append("text").text("temp").attr("font-family", charts_config.plot_attributes.axes.labels.family).attr("font-size", charts_config.plot_attributes.axes.labels.size).attr("transform", "rotate(-90)") | |
// Retrieve the rectangle encapsulating the text labels and the axes | |
var x_axis_label_box = x_axis_label.node().getBBox() | |
var y_axis_label_box = y_axis_label.node().getBBox() | |
var x_axis_box = x_axis.node().getBBox() | |
var y_axis_box = y_axis.node().getBBox() | |
// Calculate the total space consumed by these items | |
var x_axis_space_consumed = x_axis_label_box.height + x_axis_box.height + margins.axes.label_space | |
var y_axis_space_consumed = y_axis_label_box.height + y_axis_box.width + margins.axes.label_space | |
// Remove these SVG elements | |
temp.remove() | |
return {"x": x_axis_space_consumed, "y": y_axis_space_consumed} | |
} | |
function make_axes(chart_group, x_scale, y_scale, canvas, margins, maximum_drawing_space, x_label, y_label, custom_ticks=false) { | |
var axes, x_axis_label, y_axis_label, x_axis, y_axis | |
// If first call make all groups needed | |
if (chart_group.select("g.axes").empty()) { | |
axes = chart_group.append("g").attr("class", "axes") | |
x_axis_label = axes.append("text").attr("class", "x_axis_label") | |
y_axis_label = axes.append("text").attr("class", "y_axis_label") | |
x_axis= axes.append("g").attr("class", "x_axis") | |
y_axis= axes.append("g").attr("class", "y_axis") | |
} | |
axes = chart_group.select("g.axes") | |
var x_axis_scaled = d3.axisBottom().scale(x_scale).tickSize(charts_config.plot_attributes.axes.ticks.size).ticks(6) | |
var y_axis_scaled = d3.axisLeft().scale(y_scale).tickSize(charts_config.plot_attributes.axes.ticks.size).ticks(6) | |
var center = {"x": margins.x.left + margins.axes.y + maximum_drawing_space.x / 2,"y": margins.y.top + margins.buttons + margins.title + maximum_drawing_space.y / 2} | |
if (custom_ticks!=false) { | |
x_axis_scaled.tickFormat(function (d, i) {return custom_ticks[i]}).ticks(custom_ticks.length) | |
} | |
x_axis_label = axes.select("text.x_axis_label").text(x_label).attr("font-family", charts_config.plot_attributes.axes.labels.family).attr("font-size", charts_config.plot_attributes.axes.labels.size).attr("transform", "translate(" + center.x + "," + (canvas.y - margins.y.bottom)+")").attr("text-anchor", "middle") | |
y_axis_label = axes.select("text.y_axis_label").text(y_label).attr("font-family", charts_config.plot_attributes.axes.labels.family).attr("font-size", charts_config.plot_attributes.axes.labels.size).attr("transform", "translate(" + margins.x.left + "," + center.y + ") rotate(-90)").attr("text-anchor", "middle") | |
var x_axis_label_box = x_axis_label.node().getBBox() | |
var y_axis_label_box = y_axis_label.node().getBBox() | |
var x_axis_y = margins.axes.x + margins.y.bottom | |
var y_axis_x = margins.axes.y + margins.x.left | |
x_axis= axes.select("g.x_axis").transition().duration(500).call(x_axis_scaled) | |
.attr("transform", "translate(" + (margins.x.left + margins.axes.y) + "," + (canvas.y - x_axis_y)+")") | |
.attr("font-size", charts_config.plot_attributes.axes.size) | |
.attr("font-family", charts_config.plot_attributes.axes.family) | |
y_axis= axes.select("g.y_axis").transition().duration(500).call(y_axis_scaled) | |
.attr("font-size", charts_config.plot_attributes.axes.size) | |
.attr("font-family", charts_config.plot_attributes.axes.family) | |
.attr("transform", "translate(" + y_axis_x + "," + (center.y - maximum_drawing_space.y / 2)+")") | |
} |
This file contains 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 make_box_and_whiskers_chart(data) { | |
var svg, chart_group, data_extent, canvas, margins, max_draw_space, doc_state | |
var plot = "box_and_whiskers_chart" | |
var button_keys = keys(data) | |
var quantile_keys = ["min", "Q1", "Q2", "Q3", "max"] | |
svg = make_chart_svg(plot) | |
if (svg.select("g." + plot + "_group").empty()) { | |
chart_group = svg.append("g").attr("class", plot + "_group") | |
charts_config.document_state.box_and_whiskers = button_keys[0] | |
} else { | |
chart_group = svg.select("g." + plot + "_group") | |
} | |
chart_group.data(data) | |
doc_state = charts_config.document_state.box_and_whiskers | |
data = data[doc_state] | |
data_extent = { | |
"max": { | |
"x": data.length + 1, | |
"y": d3.max(data, function(d) {return d.max}) | |
}, | |
"min": { | |
"x": 0, | |
"y": d3.min(data, function(d) {return d.min}) | |
} | |
} | |
canvas = extract_canvas_from_svg(svg) | |
margins = make_margins(chart_group, canvas, data_extent, true) | |
max_draw_space = calculate_maximum_drawing_space(canvas, margins) | |
make_buttons(chart_group, margins, canvas, max_draw_space, button_keys) | |
color_buttons(chart_group, doc_state) | |
make_title(chart_group, ["Box and Whiskers Chart"], margins, canvas, max_draw_space) | |
var x_scale = d3.scaleLinear().domain([data_extent.min.x, data_extent.max.x]).range([0, max_draw_space.x]) | |
var y_scale = d3.scaleLinear().domain([data_extent.max.y, data_extent.min.y]).range([0, max_draw_space.y]) | |
var x_axis_label = "Boxes" | |
var y_axis_label = "Values" | |
// use a scale band for the axes | |
var custom_ticks = [""] | |
data.map(function(d) { custom_ticks.push(d.data)}) | |
custom_ticks.push("") | |
make_axes(chart_group, x_scale, y_scale, canvas, margins, max_draw_space, x_axis_label, y_axis_label, custom_ticks) | |
// box parameters | |
box = {} | |
box.spacing = 2 | |
box.width = max_draw_space.x / (data.length + 1) - box.spacing | |
var previous_boxes = chart_group.selectAll("g." + plot + "_box_group").size() | |
var current_boxes = data.length | |
var box_group = chart_group.selectAll("g." + plot + "_box_group").data(data) | |
var whiskers = box_group.selectAll("g.whiskers") | |
if (previous_boxes < current_boxes) { | |
var box_group = box_group.enter().append("g").attr("class", plot + "_box_group") | |
box_group.selectAll("rect").data(function(d) {return [d,d,d,d]}).enter().append("rect") | |
.attr("class", function(d,i) { | |
if (i == 0 || i == 3) { | |
return plot + "_hidden_whisker_box_rect" | |
} else { | |
return plot + "_quantile_box_rect" | |
}}) | |
var whiskers = box_group.append("g").attr("class", "whiskers") | |
whiskers.append("g").attr("class", "lower_whisker").append("path").attr("class", "whisker") | |
whiskers.append("g").attr("class", "upper_whisker").append("path").attr("class", "whisker") | |
} else if (previous_boxes > current_boxes) { | |
box_group.exit().transition().delay(function(d, i) {return 100 + i * 10}) | |
.attr("transform","translate(0,0)").remove() | |
} else { | |
var box_group = chart_group.selectAll("g." + plot + "_box_group") | |
} | |
box_group = chart_group.selectAll("g." + plot + "_box_group") | |
whiskers = box_group.selectAll("g.whiskers") | |
box_group.each(function(dat, ind) { | |
var current_quantile_group = d3.select(this) | |
var quantile_data = {"min": dat.min, "Q1": dat.Q1, "Q2": dat.Q2, "Q3": dat.Q3,"max": dat.max} | |
var low_box, high_box | |
current_quantile_group.selectAll('rect').each( | |
function(d, i){ | |
var current_rect = d3.select(this) | |
var class_name = current_rect.node().className.baseVal | |
var x_shift = box_x_shift(x_scale, ind, margins, box) | |
var y_shift = box_y_shift(y_scale, quantile_data, i, margins, box) | |
if (class_name.includes("_quantile_box")) { | |
current_rect.attr("fill", charts_config.colors.palette[ind]) | |
.attr("stroke", charts_config.plots.box_and_whiskers.stroke) | |
.style("opacity", 1).transition().delay(function(d, i) {return 100 + ind * 10}) | |
.attr("height", box_height(y_scale, quantile_data, i)) | |
.attr("width", box.width) | |
.attr("transform", "translate("+x_shift+","+y_shift+")") | |
} else { | |
current_rect.attr("fill", "white").style("opacity",0) | |
.transition().delay(function(d, i) {return 100 + ind * 10}) | |
.attr("height", box_height(y_scale, quantile_data, i)) | |
.attr("width", box.width) | |
.attr("transform", "translate("+x_shift+","+y_shift+")") | |
if (i == 0) { | |
low_box = {"x":x_shift,"y":y_shift,"h":box_height(y_scale, quantile_data, i),"w":box.width} | |
lower_whisker = make_whisker_path(low_box, false) | |
current_quantile_group.select("g.lower_whisker").select("path.whisker").transition().delay(function(d, i) {return 100 + ind * 10}).attr("d", lower_whisker).attr("fill", "none") | |
.attr("stroke", charts_config.colors.palette[ind]) | |
.attr("stroke-width", charts_config.plots.box_and_whiskers.whiskers.width) | |
} else if (i == 3) { | |
high_box = {"x":x_shift,"y":y_shift,"h":box_height(y_scale, quantile_data, i),"w":box.width} | |
upper_whisker = make_whisker_path(high_box, true) | |
current_quantile_group.select("g.upper_whisker").select("path.whisker").transition().delay(function(d, i) {return 100 + ind * 10}).attr("d", upper_whisker).attr("fill", "none") | |
.attr("stroke", charts_config.colors.palette[ind]) | |
.attr("stroke-width", charts_config.plots.box_and_whiskers.whiskers.width) | |
} | |
} | |
} | |
) | |
}) | |
box_group.selectAll("rect").on("mouseover", mouseoverFunction) | |
box_group.selectAll("rect").on("mouseout", mouseoutFunction) | |
function mouseoverFunction(d, i) { | |
var i = d3.select(this).node().className.baseVal[d3.select(this).node().className.baseVal.length - 1] | |
d3.select(this).style("opacity", function () {if (i != 0 && i != 3) {return charts_config.plots.box_and_whiskers.opacity.hover} else {return 0}}) | |
var quantile_data = {"min": d.min, "Q1": d.Q1, "Q2": d.Q2, "Q3": d.Q3,"max": d.max} | |
var box = d3.select(this).node().getBBox() | |
y = box.y + box.height / 2 | |
x = x_scale(data.indexOf(d)) + margins.x.left + margins.axes.y + box.width / 2 | |
var tooltip_text = [ | |
"Data: " + d.data, | |
"Max: " + quantile_data.max, | |
"Q3: " + quantile_data.Q3, | |
"Q2: " + quantile_data.Q2, | |
"Q1: " + quantile_data.Q1, | |
"Min: " + quantile_data.min | |
] | |
// makeTooltip(d3.select(this.parentNode), tooltipText, chartGroup, canvas, margins) | |
make_tooltip(d3.select(this), tooltip_text, chart_group, canvas, margins) | |
// makeTooltip(x, y, tooltipText, chartGroup, canvas, margins) | |
} | |
function mouseoutFunction(d, i) { | |
chart_group.select("g.tooltip_group").remove() | |
var i = d3.select(this).node().className.baseVal[d3.select(this).node().className.baseVal.length - 1] | |
// d3.select(this).style("opacity", charts_config.plots.box.opacity.nonhover) | |
d3.select(this).style("opacity", function () {if (i != 0 && i != 3) {return charts_config.plots.box_and_whiskers.opacity.nonhover} else {return 0}}) | |
} | |
chart_group.select("g.axes").raise() | |
} | |
function box_height(y_scale, extracted_data, i) { | |
var data_keys = keys(extracted_data) | |
return y_scale(extracted_data[data_keys[i]]) - y_scale(extracted_data[data_keys[i+1]]) | |
} | |
function box_x_shift(x_scale, i, margins, box) { | |
var x_shift = x_scale(i) + margins.x.left + margins.axes.y + box.width / 2 + box.spacing | |
return x_shift | |
} | |
function box_y_shift(y_scale, extracted_data, i, margins, box) { | |
var data_keys = keys(extracted_data) | |
var sum = 0 | |
for (var j = 3; j > i; j--) { | |
sum += y_scale(extracted_data[data_keys[j]]) - y_scale(extracted_data[data_keys[j+1]]) | |
} | |
var y_shift = margins.y.top + margins.title + margins.buttons + sum + y_scale(extracted_data[data_keys[4]]) | |
return y_shift | |
} | |
function make_whisker_path(box, which) { | |
var x, y, w, h | |
x = box.x | |
y = box.y | |
h = box.h | |
w = box.w | |
var p | |
if (which) { // make upper whisker | |
p = "M " + (x + w /2) + " " + (y + h) + " " | |
p += "L " + (x + w /2) + " " + (y) + " " | |
p += "L " + (x) + " " + (y) + " " | |
p += "L " + (x + w) + " " + (y) + " " | |
} else { // make lower whisker | |
p = "M " + (x + w /2) + " " + (y) + " " | |
p += "L " + (x + w /2) + " " + (y + h) + " " | |
p += "L " + (x) + " " + (y + h) + " " | |
p += "L " + (x + w) + " " + (y + h) + " " | |
} | |
return p | |
} |
This file contains 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 make_margins(chart_group, canvas, data_extent, buttons=true) { | |
var margins = { | |
"x": { | |
"left": canvas.x * 0.04, | |
"right": canvas.x * 0.04 | |
}, | |
"y": { | |
"top": canvas.x * 0.04, | |
"bottom": canvas.x * 0.04 | |
}, | |
"title": charts_config.plot_attributes.title.size * 2, // space consumed by title | |
"buttons": charts_config.plot_attributes.buttons.size * 2, // space consumed by buttons | |
"axes": { | |
"x": charts_config.plot_attributes.axes.size * 4, // space consumed by x axes | |
"y": charts_config.plot_attributes.axes.size * 4, // space consumed by y axes | |
"label_space": 10 // space between axis label and axis, included in axes.x / axes.y respectively | |
} | |
} | |
// Update axes margins based off space used by their rendered SVG elements | |
axes_margins = calculate_space_needed_by_axes(chart_group, data_extent, margins) | |
margins.axes.x = axes_margins.x | |
margins.axes.y = axes_margins.y | |
if (!buttons) {margins.buttons = 0} | |
return margins | |
} | |
function calculate_maximum_drawing_space(canvas, margins) { | |
var maximum_drawing_space = { | |
"x": canvas.x - margins.x.left - margins.x.right - margins.axes.y, | |
"y": canvas.y - margins.y.top - margins.y.bottom - margins.axes.x - margins.title - margins.buttons | |
}; | |
return maximum_drawing_space | |
} | |
function extract_canvas_from_svg(svg) { | |
var width = svg.attr("width") | |
var height = svg.attr("height") | |
var canvas = { | |
"x": parseNumber(width), | |
"y": parseNumber(height) | |
} | |
return canvas | |
} |
This file contains 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 make_chart_svg(plot) { | |
var section = d3.select("#"+plot) | |
var chart_width = parseNumber(charts_config.svg.width) | |
var chart_height = parseNumber(charts_config.svg.height) | |
var chart_width_string, chart_height_string | |
if (typeofNumber(chart_width) == "float") { | |
chart_width_string = (chart_width * section.node().clientWidth ) + "px" | |
} else { | |
chart_width_string = chart_width + "px" | |
} | |
if (typeofNumber(chart_height) == "float") { | |
chart_height_string = (chart_height * section.node().clientHeight ) + "px" | |
} else { | |
chart_height_string = chart_height + "px" | |
} | |
var svg | |
if (section.select("#" + plot + "_svg").empty()) { | |
svg = section.append("svg") | |
} else { | |
svg = section.select("#" + plot + "_svg") | |
} | |
svg.attr("perserveAspectRatio", "xMinYMid meet") | |
.classed("svg-content-responsive", true) | |
.attr("id", plot + "_svg") | |
.attr("width", chart_width_string) | |
.attr("height", chart_height_string) | |
return svg | |
} | |
function update_chart_svg(plot) { | |
var section = document.getElementById(plot + "_chart") | |
var svg = d3.select("svg#" + plot + "_svg") | |
var chart_width = parseNumber(charts_config.svg.width) | |
var chart_height = parseNumber(charts_config.svg.height) | |
var chart_width_string, chart_height_string | |
if (typeofNumber(chart_width) == "float") { | |
chart_width_string = (chart_width * section.clientWidth ) + "px" | |
} else { | |
chart_width_string = chart_width + "px" | |
} | |
if (typeofNumber(chart_height) == "float") { | |
chart_height_string = (chart_height * section.clientHeight ) + "px" | |
} else { | |
chart_height_string = chart_height + "px" | |
} | |
return svg | |
} |
This file contains 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
//-------------------------------------------------------------------// | |
// // | |
// MAKE TITLE // | |
// // | |
//-------------------------------------------------------------------// | |
function make_title(chart_group, text_array, margins, canvas, maximum_drawing_space) { | |
// Does chart title already exist? | |
if (!chart_group.select("g.chart_title").empty()) { | |
// Yes. Clear Title | |
chart_group.select("g.chart_title").remove() | |
// Reset Margins | |
margins.title = charts_config.plot_attributes.title.size * 2 | |
} | |
// Construct full title | |
var full_title = text_array.join(" ") | |
// Store title lines | |
var title_lines = [] | |
// Max line length | |
var characters_per_line = (canvas.x - margins.x.right) / (charts_config.plot_attributes.title.size / 2) | |
while (full_title.length > 0) { | |
var slice_position = characters_per_line - 1 | |
var line_slice = full_title.slice(0, slice_position) | |
var last_space = line_slice.lastIndexOf(" ") | |
// space is first character, drop it | |
if (line_slice[0] == " ") { | |
full_title = full_title.slice(1, full_title.length) | |
line_slice = full_title.slice(0, slice_position) | |
last_space = line_slice.lastIndexOf(" ") | |
} | |
if (full_title[slice_position + 1] != " " & slice_position < full_title.length) { | |
// the leading character of next splice is not a space (e.g. breaks a word) | |
// and there is more in the title to come | |
if (last_space == -1) { // no spaces in this line, we have broken a word | |
line_slice = full_title.slice(0, slice_position - 1) + "-" | |
slice_position -= 1 | |
} else { // there is a space, truncate to that space | |
slice_position = last_space | |
line_slice = full_title.slice(0, slice_position) | |
} | |
last_space = line_slice.lastIndexOf(" ") | |
} else if (slice_position < full_title.length & last_space < line_slice.length) { | |
// last word is split, so add a hypen | |
line_slice = full_title.slice(0, slice_position - 1) | |
slice_position -= 1 | |
last_space = line_slice.lastIndexOf(" ") | |
// if the word is a two letter word, e.g. the last letter in the string is | |
// the first letter of the two letter word, then that letter is droped for | |
// a hypen before a space. That makes no sense, so drop the entire 2 letter word | |
if (last_space == slice_position - 1) { | |
// if space is last character drop it | |
line_slice = full_title.slice(0, slice_position - 1) | |
slice_position -= 1 | |
} else { | |
line_slice += "-" | |
slice_position -= 1 | |
} | |
last_space = line_slice.lastIndexOf(" ") | |
} | |
if (last_space == slice_position) { | |
// if space is last character drop it | |
line_slice = full_title.slice(0, slice_position - 1) | |
slice_position -= 1 | |
} | |
title_lines.push(line_slice) | |
full_title = full_title.slice(slice_position, full_title.length) | |
} | |
var chart_title = chart_group.append("g").attr("class", "chart_title") | |
for (var i = 0; i < title_lines.length; i++) { | |
chart_title.append("text") | |
.attr("class", "chartTitle") | |
.text(title_lines[i]) | |
.attr("text-anchor", "middle") | |
.attr("font-size", charts_config.plot_attributes.title.size) | |
.attr("font-family", charts_config.plot_attributes.title.family) | |
.attr("x", margins.axes.y + margins.x.left + maximum_drawing_space.x / 2) | |
.attr("y", margins.y.top + margins.buttons + (charts_config.plot_attributes.title.size * (i) + charts_config.plot_attributes.title.size / 2)) | |
margins.title = chart_title.node().getBBox().height + charts_config.plot_attributes.title.size | |
maximum_drawing_space.y = canvas.y - margins.y.top - margins.y.bottom - margins.axes.x - margins.title - margins.buttons | |
} | |
} |
This file contains 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
//-------------------------------------------------------------------// | |
// // | |
// MAKE TOOLTIP // | |
// // | |
//-------------------------------------------------------------------// | |
function make_tooltip(obj, text_array, chart_group, canvas, margins) { | |
// get the x and y coordinates of the object to apply tooltip too | |
var x = obj.node().transform.baseVal.consolidate().matrix.e | |
var y = obj.node().transform.baseVal.consolidate().matrix.f | |
// for convenience | |
var tooltip_config = charts_config.plot_attributes.tooltip | |
var sep = tooltip_config.default_seperation_from_object | |
// Add the tooltip to the chart and as a child - the text group | |
var tooltip = chart_group.append("g").attr("class", "tooltip_group") | |
var text_group = tooltip.append("g").attr("class", "tooltip_text_group") | |
// Add text in reverse order placing low to high | |
text_array.reverse() | |
for (var i = 0; i < text_array.length; i++) { | |
var text = text_group.append("text") | |
.text(text_array[i]) | |
.attr("font-size", tooltip_config.size) | |
.attr("text-anchor", "middle") | |
.attr("x", x) | |
.attr("y", y - sep - tooltip_config.curve - (i * tooltip_config.size)) | |
.attr("class", "tooltipText") | |
.attr("fill", tooltip_config.emphasis) | |
.attr("font-family", tooltip_config.family) | |
.attr("font-weight", "normal") | |
// The first line in the tooltip gets different coloration | |
if (i < text_array.length - 1) { | |
text.attr("fill", tooltip_config.text) | |
.attr("font-family", tooltip_config.family) | |
.attr("font-weight", "normal") | |
} | |
} | |
// Make the bubble around the text | |
var bubble = tooltip.append("path") | |
.attr("d", make_tooltip_bubble(text_group)) | |
.attr("fill", tooltip_config.fill) | |
.attr("stroke", tooltip_config.stroke) | |
.attr("opacity", tooltip_config.opacity) | |
// Text goes in front of the box | |
text_group.raise() | |
// Get the bounding box of the bubble | |
var bubble_box = bubble.node().getBBox() | |
// Calculate the limits to contain the tooltip | |
var limits = { | |
"top": margins.y.top + margins.buttons + margins.title, | |
"bottom": canvas.y - margins.axes.x - margins.y.bottom, | |
"left": margins.x.left + margins.axes.y, | |
"right": canvas.x - margins.x.right | |
} | |
// Get the boundaries of the object | |
var object_boundaries = { | |
"left": x, | |
"right": x + obj.node().getBBox().width, | |
"top": y, | |
"bottom": y + obj.node().getBBox().height | |
} | |
// Calculate putative tooltip placements | |
var tooltip_placements = { | |
"upper_left": {"x": object_boundaries.left - bubble_box.width - sep, "y":object_boundaries.top - bubble_box.height - sep}, | |
"upper_right": {"x":object_boundaries.right + sep, "y": object_boundaries.top - bubble_box.height - sep}, | |
"lower_left": {"x":object_boundaries.left - bubble_box.width - sep, "y": object_boundaries.bottom + sep}, | |
"lower_right": {"x":object_boundaries.right + sep, "y":object_boundaries.bottom + sep} | |
} | |
// Figure out which placements fall within the limits | |
var valid_placments = { | |
"upper_left": {"x": tooltip_placements.upper_left.x > limits.left, "y": tooltip_placements.upper_left.y > limits.top}, | |
"upper_right": {"x": tooltip_placements.upper_right.x + bubble_box.width < limits.right, "y": tooltip_placements.upper_right.y > limits.top}, | |
"lower_left": {"x": tooltip_placements.lower_left.x > limits.left, "y": tooltip_placements.lower_left.y + bubble_box.height < limits.bottom}, | |
"lower_right": {"x": tooltip_placements.lower_right.x + bubble_box.width < limits.right, "y": tooltip_placements.lower_right.y + bubble_box.height < limits.bottom} | |
} | |
var placements = keys(valid_placments) | |
var tooltip_placement | |
for (var i = 0; i < placements.length; i++) { | |
var current_placement = valid_placments[placements[i]] | |
if (current_placement.x && current_placement.y) { | |
tooltip_placement = tooltip_placements[placements[i]] | |
break | |
} | |
} | |
// Reposition the tooltip to its correct location | |
tooltip.attr("transform", "translate("+(tooltip_placement.x - bubble_box.x)+","+(tooltip_placement.y - bubble_box.y)+")") | |
} | |
function transform_values(selection) { | |
return { | |
"x": selection.node().transform.baseVal.consolidate().matrix.e, | |
"y": selection.node().transform.baseVal.consolidate().matrix.f | |
} | |
} | |
function make_tooltip_bubble(text_group) { | |
var textBox = text_group.node().getBBox() | |
var x = textBox.x | |
var y = textBox.y | |
var width = textBox.width | |
var height = textBox.height | |
var point = charts_config.plot_attributes.tooltip.point | |
var curve = charts_config.plot_attributes.tooltip.curve | |
// Start at bottom center and work around left - up - right - down - close | |
d = "M " + (x + width / 2) + " " + (y + height + curve) | |
// go left | |
d += "l -" + (width / 2) + " 0 " | |
// curve left and up | |
d += "q -" + curve + " 0 -" + curve + " -" + curve + " " | |
// go up | |
d += "l 0 -" + (height) + " " | |
// curve up and right | |
d += "q 0 -" + curve + " " + curve + " -" + curve + " " | |
// go right | |
d += "l " + (width) + " 0 " | |
// curve right and down | |
d += "q " + curve + " 0 " + curve + " " + curve + " " | |
// go down | |
d += "l 0 " + (height) + " " | |
// curve down and left | |
d += "q 0 " + curve + " -" + curve + " " + curve + " " | |
// go left | |
d += "l -" + (width / 2) + " 0 " | |
// close | |
d += " z" | |
return d | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment