Skip to content

Instantly share code, notes, and snippets.

@SumNeuron
Last active April 30, 2017 13:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SumNeuron/262e37e2f932cf4b693f241c52a410ff to your computer and use it in GitHub Desktop.
Save SumNeuron/262e37e2f932cf4b693f241c52a410ff to your computer and use it in GitHub Desktop.
D3 (v4) Flexible and Responsive Box and Whisker Chart
{
"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}
]
}
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)
{
"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"
]
}
}
/**************************************************************************
* *
* 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"
}
}
<!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>
//-------------------------------------------------------------------//
// //
// 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)+")")
}
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
}
//-------------------------------------------------------------------//
// //
// MAKE SVG BUTTONS //
// //
//-------------------------------------------------------------------//
// Makes radio buttons for changing chart
function make_buttons(chart_group, margins, canvas, maximum_drawing_space, button_array) {
var radius = charts_config.plot_attributes.buttons.size / 2 // radius is half button font size
var x = margins.axes.y + margins.x.left + radius // starting x locations
var y = radius + margins.y.top
var button_group
if (chart_group.selectAll("g.buttons_group").empty()) {
chart_group.append("g") // group for all buttons
.attr("class", "buttons_group")
.selectAll("g.buttons_group")
.data(button_array)
.enter()
.append("g") // group for each button (circle and text)
.attr("class", "button_group")
var button_group = chart_group.selectAll("g.button_group")
// add the circles
button_group.append("circle")
.attr("r", radius)
.attr("class", "button_circle")
.attr("class", function(d, i) {return "button_circle_number" + i})
.on("click", button_click)
// add the text
button_group.append("text")
.attr("class", "button_text")
.text(function(d) {return d})
.attr("class", function(d, i) {return "button_text_number" + i})
.on("click", button_click)
}
button_group = chart_group.selectAll("g.button_group")
// position the circles and text
button_group.each(function(d, i) {
var current_group = d3.select(this)
var current_circle = current_group.select("circle")
var current_text = current_group.select("text")
if (i == 0) {
current_circle.attr("cx", x)
.attr("cy", y)
.attr("fill", charts_config.plot_attributes.buttons.fill.not_selected)
.attr("stroke", charts_config.plot_attributes.buttons.stroke)
current_text.attr("x", x + radius * 2)
.attr("y", y + radius / 2)
.text(function(d, i) {return d})
.attr("font-family", charts_config.plot_attributes.buttons.family)
.attr("font-size", charts_config.plot_attributes.buttons.size)
margins.buttons = current_text.node().getBBox().y + current_text.node().getBBox().height + radius
} else {
var previous_text = button_group.select(".button_text_number" + (i-1))
var previous_circle = button_group.select(".button_circle_number" + (i-1))
var previous_text_box = previous_text.node().getBBox()
var previous_circle_box = previous_circle.node().getBBox()
var cx = previous_text_box.x + previous_text_box.width + radius * 2
var cy = previous_circle_box.y + radius
current_circle.attr("cx", cx)
.attr("cy", cy)
.attr("fill", charts_config.plot_attributes.buttons.fill["not_selected"])
.attr("stroke", charts_config.plot_attributes.buttons.stroke)
current_text.attr("x", cx + radius * 2)
.attr("y", cy + radius / 2)
.text(function(d, i) {return d})
.attr("font-family", charts_config.plot_attributes.buttons.family)
.attr("font-size", charts_config.plot_attributes.buttons.size)
var far_right_point = current_text.node().getBBox().x + current_text.node().getBBox().width
var farthest_point_to_draw = canvas.x - margins.x.right
if (far_right_point > farthest_point_to_draw) { //
cx = x
cy = previous_circle_box.y + radius * 4
current_circle.attr("cx", cx)
.attr("cy", cy)
current_text.attr("x", cx + radius * 2)
.attr("y", cy + radius / 2)
}
margins.buttons = current_text.node().getBBox().y + current_text.node().getBBox().height + radius
}
})
}
function button_click() {
var current = d3.select(this).datum()
charts_config.document_state.box_and_whiskers = current
make_box_and_whiskers_chart(box_and_whiskers_data)
}
function color_buttons(chart_group, state) {
chart_group.select("g.buttons_group").selectAll("circle").attr("fill", function (d, i) {
if (d == state) {
return charts_config.plot_attributes.buttons.fill.selected
}
return charts_config.plot_attributes.buttons.fill.not_selected
})
}
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
}
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
}
//-------------------------------------------------------------------//
// //
// 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
}
}
//-------------------------------------------------------------------//
// //
// 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