|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<style> |
|
svg { |
|
font: 10px sans-serif; |
|
} |
|
|
|
#chartContainer { |
|
top: 0; |
|
left: 0; |
|
width: 960px; |
|
height: 500px; |
|
} |
|
|
|
#chartContainer svg { |
|
margin-top:10px; |
|
} |
|
|
|
div.tooltip { |
|
position: absolute; |
|
border: 2px solid #333; |
|
border-radius: 8px; |
|
padding: 5px; |
|
background-color: #fff; |
|
z-index: 2000; |
|
font-family: Helvetica; |
|
font-size: 12px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="chartContainer"></div> |
|
<script type="text/javascript"> |
|
//***************************************************************************************************** |
|
// 1) d3.js CHART SETUP |
|
//***************************************************************************************************** |
|
{ |
|
var graph_options = { |
|
"TickLabel": "%", |
|
"AmberValue": "40.0", |
|
"RedValue": "20.0", |
|
"Operator": "<", |
|
"AlwaysStartAtZero": true, |
|
"AxisBuffer": 5 |
|
}; |
|
|
|
// set margin, width and height for the graph element of the SVG |
|
// fill chartContainer |
|
var margin = { top: 60, right: 60, bottom: 60, left: 60 }, |
|
width = d3.select("#chartContainer").node().offsetWidth - margin.left - margin.right, |
|
height = d3.select("#chartContainer").node().offsetHeight - margin.top - margin.bottom; |
|
|
|
// set up the scale for the x and y axis |
|
var x = d3.time.scale() |
|
.range([0, width]), |
|
y = d3.scale.linear() |
|
.range([height, 0]); |
|
|
|
var color = d3.scale.category10(); |
|
|
|
// define if "Green" area of the chart should be filled |
|
var showGreenBand = true; |
|
|
|
// define legend box size and space |
|
var legendRectHeight = 1; |
|
var legendRectWidth = 30; |
|
var legendSpacing = 10; |
|
|
|
// width of lines |
|
var pathStrokeWidth = "2px"; |
|
|
|
// tickFormat of the x-Axis, and the positioning of the |
|
// (rotated) tick label |
|
var x_tickFormat = d3.time.format("%H:%M"); |
|
var ticks = 24; |
|
var y_tick_y = 10; |
|
var y_tick_x = -30; |
|
|
|
// scale and format the x-Axis |
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient("bottom") |
|
.ticks(ticks) |
|
.innerTickSize(-height) // shows the grid lines |
|
.outerTickSize(0) |
|
.tickFormat(x_tickFormat); |
|
|
|
// scale and format the y-Axis |
|
var yAxis = d3.svg.axis() |
|
.scale(y) |
|
.orient("left") |
|
.innerTickSize(-width) // shows the grid lines |
|
.outerTickSize(0) |
|
.tickFormat(function(d) { return d + graph_options.TickLabel }); |
|
|
|
// define what data the line will represent |
|
var line = d3.svg.line() |
|
.x(function(d) { return x(d.Time); }) |
|
.y(function(d) { |
|
// does not draw lines beyond the axis domain (important if adding trend lines to charts that should only go to 100%) |
|
if (d.Value > y.domain()[0]) { |
|
if (d.Value < y.domain()[1]) { |
|
return y(d.Value); |
|
} else return y(y.domain()[1]); |
|
} else return y(y.domain()[0]); |
|
}) |
|
.interpolate("linear"); |
|
|
|
|
|
// append the svg element to the chart container (#chartcontainer) |
|
var svg = d3.select("#chartContainer").append("svg") |
|
.attr("id", "chartSVG") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.attr("xmlns", "http://www.w3.org/2000/svg") |
|
.attr("version", "1.1") |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
// append the tooltip DOM element |
|
var tooltip = d3.select("body").append("div") |
|
.attr("id", "tooltip") |
|
.attr("class", "tooltip") |
|
.style("position", "absolute") |
|
.style("opacity", 0); |
|
} |
|
|
|
//***************************************************************************************************** |
|
// 2) d3.js DROP SHADOW FILTERING |
|
//***************************************************************************************************** |
|
{ |
|
// filters go in defs element |
|
var defs = svg.append("defs"); |
|
var stdDeviation = 3; |
|
|
|
// create filter with id #drop-shadow |
|
// height=130% so that the shadow is not clipped |
|
// filterUnits=userSpaceOnUse so that straight lines are also visible |
|
var filter = defs.append("filter") |
|
.attr("id", "drop-shadow") |
|
.attr("height", "130%") |
|
.attr("filterUnits", "userSpaceOnUse") |
|
|
|
// SourceAlpha refers to opacity of graphic that this filter will be applied to |
|
// convolve that with a Gaussian with stdDeviation (3px) and store result |
|
// in blur |
|
filter.append("feGaussianBlur") |
|
.attr("in", "SourceAlpha") |
|
.attr("stdDeviation", stdDeviation) |
|
.attr("result", "blur"); |
|
|
|
// translate output of Gaussian blur to the right and downwards with stdDeviation |
|
// store result in offsetBlur |
|
filter.append("feOffset") |
|
.attr("in", "blur") |
|
.attr("dx", stdDeviation) |
|
.attr("dy", stdDeviation) |
|
.attr("result", "offsetBlur"); |
|
|
|
// overlay original SourceGraphic over translated blurred opacity by using |
|
// feMerge filter. Order of specifying inputs is important! |
|
var feMerge = filter.append("feMerge"); |
|
|
|
feMerge.append("feMergeNode") |
|
.attr("in", "offsetBlur") |
|
feMerge.append("feMergeNode") |
|
.attr("in", "SourceGraphic"); |
|
} |
|
|
|
//***************************************************************************************************** |
|
// 3) CHART COLOR BANDING |
|
//***************************************************************************************************** |
|
{ |
|
function pathBandData(yStart, yEnd) { |
|
// check to see if band falls outside of the y-Axis scale, if so, return nothing |
|
if (yStart > y.domain()[1] && yEnd > y.domain()[1]) { |
|
return null; |
|
} else // otherwise, draw the banding box. still check we don't go beyond the y-Axis scale at each point. |
|
{ |
|
return "M" + x(x.domain()[0]) + "," + (yStart > y.domain()[1] ? y(y.domain()[1]) : y(yStart)) + |
|
"L" + x(x.domain()[0]) + "," + (yEnd > y.domain()[1] ? y(y.domain()[1]) : y(yEnd)) + |
|
"L" + x(x.domain()[1]) + "," + (yEnd > y.domain()[1] ? y(y.domain()[1]) : y(yEnd)) + |
|
"L" + x(x.domain()[1]) + "," + (yStart > y.domain()[1] ? y(y.domain()[1]) : y(yStart)); |
|
} |
|
} |
|
|
|
function appendColorBands() { |
|
var GreenValue = y.domain()[0]; |
|
var EndValue = y.domain()[1]; |
|
if (graph_options.Operator == "<") { |
|
// SWAP RED/GREEN START POINTS IF LESS IS WORSE |
|
GreenValue = y.domain()[1]; |
|
EndValue = y.domain()[0]; |
|
} |
|
if (showGreenBand) { |
|
svg.append("g").append("path") // GREEN |
|
.attr("d", pathBandData(GreenValue, graph_options.AmberValue)) |
|
.style("opacity", 0.1) |
|
.style("stroke", "#005e23") |
|
.style("fill", "#005e23"); |
|
} |
|
svg.append("g").append("path") |
|
.attr("d", pathBandData(graph_options.AmberValue, graph_options.RedValue)) |
|
.style("opacity", 0.1) |
|
.style("stroke", "#f90") |
|
.style("fill", "#f90"); |
|
svg.append("g").append("path") |
|
.attr("d", pathBandData(graph_options.RedValue, EndValue)) |
|
.style("opacity", 0.1) |
|
.style("stroke", "#c03") |
|
.style("fill", "#c03"); |
|
} |
|
} |
|
//***************************************************************************************************** |
|
// 4) GRAPH GENERATION |
|
//***************************************************************************************************** |
|
d3.json("performance.json", function(error, data) { |
|
// ensure Time and Value are properly treated as numbers |
|
data.forEach(function(d) { |
|
d.Time = +d.Time; |
|
d.Value = +d.Value; |
|
}); |
|
|
|
// nest the results based on the DriveLetter (e.g.; C:) |
|
data = d3.nest().key(function(d) { |
|
return d.DriveLetter; |
|
}).entries(data); |
|
|
|
// findDateBoundary sets the X values so the graphs will always end on a full day |
|
// If param t set to 0, returns midnight, 1 set to start of next day |
|
function findDateBoundary(d, t) { |
|
var dS = new Date(d); |
|
var dT = new Date(dS.getFullYear(), dS.getMonth(), dS.getDate(), 0, 0, 0); |
|
if (t) { |
|
dT.setTime(dT.getTime() + 86400000); |
|
} |
|
return dT; |
|
} |
|
|
|
// dateFormat for tooltips |
|
var dateFormat = d3.time.format("%d/%m/%Y %H:%M"); |
|
|
|
function formatDate(d) { |
|
dF = new Date(d); |
|
return dateFormat(dF); |
|
} |
|
|
|
x.domain([ |
|
findDateBoundary(d3.min(data, function(d) { return d3.min(d.values, function(d) { return d.Time }) }), 0), |
|
findDateBoundary(d3.max(data, function(d) { return d3.max(d.values, function(d) { return d.Time }) }), 1) |
|
]); |
|
var ydomainMin = graph_options.AlwaysStartAtZero ? 0 : d3.min(data, function(d) { return d3.min(d.values, function(d) { return d.Value }) }) - graph_options.AxisBuffer; |
|
y.domain([ydomainMin, d3.max(data, function(d) { return d3.max(d.values, function(d) { return d.Value }) }) + graph_options.AxisBuffer]); |
|
|
|
// append the color bands |
|
// this comes before we append the axis so that it is rendered behind the axis lines |
|
appendColorBands(); |
|
|
|
// append the x-Axis and draw the text labels. Text labels are positioned according to the |
|
// previously calculated x-Axis ticks, and rotated at -45 degrees |
|
svg.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.style("shape-rendering", "crispEdges") |
|
.call(xAxis) |
|
.selectAll("text") |
|
.attr("y", y_tick_y) |
|
.attr("x", y_tick_x) |
|
.attr("dy", ".35em") |
|
.attr("transform", "rotate(-45)") |
|
.style("text-anchor", "start") |
|
.style("text-rendering", "optimiseSpeed"); |
|
|
|
// append the y-Axis |
|
svg.append("g") |
|
.attr("class", "y axis") |
|
.call(yAxis) |
|
.style("shape-rendering", "crispEdges"); |
|
|
|
// ensure all CSS formatting is explicitly applied to all axis elements - prevents buggy rendering and svg can be exported neatly |
|
svg.selectAll(".axis path").style("fill", "none").style("stroke", "#000").style("shape-rendering", "crispEdges"); |
|
svg.selectAll(".axis line").style("fill", "none").style("stroke", "#000").style("shape-rendering", "crispEdges"); |
|
svg.selectAll(".axis .tick line").style("opacity", 0.2); |
|
svg.selectAll(".axis .tick text").style("font-family", "sans-serif").style("font-size", "10px"); |
|
|
|
// add title to the graph |
|
var title = svg.append("text") |
|
.attr("x", (width / 2)) |
|
.attr("y", 0 - (margin.top)) |
|
.attr("text-anchor", "middle") |
|
.style("font-size", "13px") |
|
.style("text-decoration", "none"); |
|
// first line (eg; server name) |
|
title.append("tspan") |
|
.attr("x", (width / 2)) |
|
.attr("dy", 12) |
|
.text("SERVER1"); |
|
// second line (description) |
|
title.append("tspan") |
|
.attr("x", (width / 2)) |
|
.attr("dy", 20) |
|
.text("Disk Space Available"); |
|
|
|
// select the data for the line chart, draw the line, and create tooltip hotspots |
|
{ |
|
// selects the data as nested by resource |
|
var DriveLetters = svg.selectAll(".DriveLetter") |
|
.data(data, function(d, i) { return d.key; }) |
|
.enter().append("g") |
|
.attr("class", "DriveLetter") |
|
.attr("id", function(d, i) { return "chart-entry-" + i }); |
|
|
|
// draws the actual line for the chart |
|
DriveLetters.append("path") |
|
.attr("class", "line") |
|
.attr("d", function(d) { return line(d.values); }) |
|
.style("stroke", function(d, i) { return color(i); }) |
|
.style("fill", "none") |
|
.style("stroke-width", pathStrokeWidth) |
|
.style("filter", "url(#drop-shadow)"); |
|
|
|
// add tooltip hit circles for each data point |
|
// each circle is drawn with 0 opacity, so that the line is not obscured |
|
var dots = DriveLetters.selectAll("circle") |
|
.data(function(d) { return d.values; }) |
|
.enter().append("circle") |
|
.attr("cx", function(d, i) { return x(d.Time); }) |
|
.attr("cy", function(d, i) { |
|
if (d.Value > y.domain()[0]) { |
|
if (d.Value < y.domain()[1]) { |
|
return y(d.Value); |
|
} else return y(y.domain()[1]); |
|
} else return y(y.domain()[0]); |
|
}) |
|
.attr("r", 3) |
|
.style("opacity", 0) |
|
.on("mouseover", function(d) { // onMouseOver() - expand the circle, set and show the tooltip |
|
d3.select(this).style("opacity", 0.7); |
|
d3.select(this).attr("r", 5); /* expand the point circle to be 5 pixels wide, |
|
so slight movement of the mouse doesn't hide it again */ |
|
// set the tooltip text |
|
tooltip.html(d.DriveLetter + " <strong>" + d.Value.toFixed(2) + graph_options.TickLabel + "</strong><br/>" + formatDate(d.Time) + " ") |
|
.style("left", (d3.event.pageX - 100) + "px") |
|
.style("top", (d3.event.pageY + 8) + "px"); |
|
// fadeIn the tooltip |
|
tooltip.transition() |
|
.duration(100) |
|
.style("opacity", 0.9) |
|
}) |
|
.on("mouseout", function(d) { // onMouseOut() - shrink the circle, hide the tooltip |
|
d3.select(this).style("opacity", 0); |
|
d3.select(this).attr("r", 3); // shrink point circle back to 3px |
|
tooltip.transition() |
|
.duration(200) |
|
.style("opacity", 0); // fadeOut the tooltip |
|
tooltip.style("left", "-9999") // move the tooltip off screen so that we don't obscure the graph circles |
|
}); |
|
} |
|
|
|
// legend generation |
|
data.forEach(function(d, i) { |
|
// adds the text element with the Drive Letter |
|
svg.append("text") |
|
.attr("class", "legend") |
|
.attr("id", "legend-text-" + i) |
|
.attr("x", width - legendSpacing) |
|
.attr("y", ((legendSpacing * i) * 2) - (margin.top / 2)) |
|
.style("fill", function() { return color(i) }) |
|
.style("font-family", "sans-serif") |
|
.style("font-size", "10px") |
|
.text(d.key); |
|
// adds line in the same color as used in the graph |
|
var thisItem = d3.select("#legend-text-" + i).node(); |
|
var bb = thisItem.getBBox(); |
|
var bx = bb.x - bb.width - legendRectWidth; |
|
svg.append("path") |
|
.attr("class", "legend") |
|
.attr("data-legend-key", i) |
|
.attr("data-color", function() { return color(i) }) |
|
.attr("d", "M" + (bb.x - legendSpacing - legendRectWidth) + "," + (bb.y + bb.height / 2) + " L" + (bb.x - legendSpacing) + "," + (bb.y + bb.height / 2)) |
|
.style("stroke", function() { return color(i) }) |
|
.style("stroke-width", "2px") |
|
.style("fill", "none") |
|
.attr("height", legendRectHeight) |
|
.attr("width", legendRectWidth) |
|
.style("filter", "url(#drop-shadow)"); |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |