Skip to content

Instantly share code, notes, and snippets.

@nethoncho
Forked from rkirsling/LICENSE
Last active December 20, 2017 14:52
Show Gist options
  • Save nethoncho/fbd6d9f649f7868831052349a45f42d3 to your computer and use it in GitHub Desktop.
Save nethoncho/fbd6d9f649f7868831052349a45f42d3 to your computer and use it in GitHub Desktop.
Trend Chart (Area + Line)

This "trend chart" shows loading times across releases of a hypothetical web application.

It is a mixture of a line chart and an area chart, with the daily median loading time indicated by the line and percentile ranges indicated by the surrounding areas.

The "lollipops" mark releases of (different parts of) the application.

demo

@import url(//fonts.googleapis.com/css?family=Open+Sans:400,700);
svg {
font: 14px 'Open Sans';
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
fill: #000;
}
.axis .tick line {
stroke: rgba(0, 0, 0, 0.1);
}
.area {
stroke-width: 1;
}
.area.outer,
.legend .outer {
fill: rgba(230, 230, 255, 0.8);
stroke: rgba(216, 216, 255, 0.8);
}
.area.inner,
.legend .inner {
fill: rgba(127, 127, 255, 0.8);
stroke: rgba(96, 96, 255, 0.8);
}
.median-line,
.legend .median-line {
fill: none;
stroke: #000;
stroke-width: 3;
}
.legend .legend-bg {
fill: rgba(0, 0, 0, 0.5);
stroke: rgba(0, 0, 0, 0.5);
}
.marker.client .marker-bg,
.marker.client path {
fill: rgba(255, 127, 0, 0.8);
stroke: rgba(255, 127, 0, 0.8);
stroke-width: 3;
}
.marker.server .marker-bg,
.marker.server path {
fill: rgba(0, 153, 51, 0.8);
stroke: rgba(0, 153, 51, 0.8);
stroke-width: 3;
}
.marker path {
fill: none;
}
.legend text,
.marker text {
fill: #fff;
font-weight: bold;
}
.marker text {
text-anchor: middle;
}
function addAxesAndLegend (svg, xAxis, yAxis, margin, chartWidth, chartHeight) {
var legendWidth = 200,
legendHeight = 100;
// clipping to make sure nothing appears behind legend
svg.append('clipPath')
.attr('id', 'axes-clip')
.append('polygon')
.attr('points', (-margin.left) + ',' + (-margin.top) + ' ' +
(chartWidth - legendWidth - 1) + ',' + (-margin.top) + ' ' +
(chartWidth - legendWidth - 1) + ',' + legendHeight + ' ' +
(chartWidth + margin.right) + ',' + legendHeight + ' ' +
(chartWidth + margin.right) + ',' + (chartHeight + margin.bottom) + ' ' +
(-margin.left) + ',' + (chartHeight + margin.bottom));
var axes = svg.append('g')
.attr('clip-path', 'url(#axes-clip)');
axes.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis);
axes.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end')
.text('Time (s)');
var legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', 'translate(' + (chartWidth - legendWidth) + ', 0)');
legend.append('rect')
.attr('class', 'legend-bg')
.attr('width', legendWidth)
.attr('height', legendHeight);
legend.append('rect')
.attr('class', 'outer')
.attr('width', 75)
.attr('height', 20)
.attr('x', 10)
.attr('y', 10);
legend.append('text')
.attr('x', 115)
.attr('y', 25)
.text('5% - 95%');
legend.append('rect')
.attr('class', 'inner')
.attr('width', 75)
.attr('height', 20)
.attr('x', 10)
.attr('y', 40);
legend.append('text')
.attr('x', 115)
.attr('y', 55)
.text('25% - 75%');
legend.append('path')
.attr('class', 'median-line')
.attr('d', 'M10,80L85,80');
legend.append('text')
.attr('x', 115)
.attr('y', 85)
.text('Median');
}
function drawPaths (svg, data, x, y) {
var upperOuterArea = d3.svg.area()
.interpolate('basis')
.x (function (d) { return x(d.date) || 1; })
.y0(function (d) { return y(d.pct95); })
.y1(function (d) { return y(d.pct75); });
var upperInnerArea = d3.svg.area()
.interpolate('basis')
.x (function (d) { return x(d.date) || 1; })
.y0(function (d) { return y(d.pct75); })
.y1(function (d) { return y(d.pct50); });
var medianLine = d3.svg.line()
.interpolate('basis')
.x(function (d) { return x(d.date); })
.y(function (d) { return y(d.pct50); });
var lowerInnerArea = d3.svg.area()
.interpolate('basis')
.x (function (d) { return x(d.date) || 1; })
.y0(function (d) { return y(d.pct50); })
.y1(function (d) { return y(d.pct25); });
var lowerOuterArea = d3.svg.area()
.interpolate('basis')
.x (function (d) { return x(d.date) || 1; })
.y0(function (d) { return y(d.pct25); })
.y1(function (d) { return y(d.pct05); });
svg.datum(data);
svg.append('path')
.attr('class', 'area upper outer')
.attr('d', upperOuterArea)
.attr('clip-path', 'url(#rect-clip)');
svg.append('path')
.attr('class', 'area lower outer')
.attr('d', lowerOuterArea)
.attr('clip-path', 'url(#rect-clip)');
svg.append('path')
.attr('class', 'area upper inner')
.attr('d', upperInnerArea)
.attr('clip-path', 'url(#rect-clip)');
svg.append('path')
.attr('class', 'area lower inner')
.attr('d', lowerInnerArea)
.attr('clip-path', 'url(#rect-clip)');
svg.append('path')
.attr('class', 'median-line')
.attr('d', medianLine)
.attr('clip-path', 'url(#rect-clip)');
}
function addMarker (marker, svg, chartHeight, x) {
var radius = 32,
xPos = x(marker.date) - radius - 3,
yPosStart = chartHeight - radius - 3,
yPosEnd = (marker.type === 'Client' ? 80 : 160) + radius - 3;
var markerG = svg.append('g')
.attr('class', 'marker '+marker.type.toLowerCase())
.attr('transform', 'translate(' + xPos + ', ' + yPosStart + ')')
.attr('opacity', 0);
markerG.transition()
.duration(1000)
.attr('transform', 'translate(' + xPos + ', ' + yPosEnd + ')')
.attr('opacity', 1);
markerG.append('path')
.attr('d', 'M' + radius + ',' + (chartHeight-yPosStart) + 'L' + radius + ',' + (chartHeight-yPosStart))
.transition()
.duration(1000)
.attr('d', 'M' + radius + ',' + (chartHeight-yPosEnd) + 'L' + radius + ',' + (radius*2));
markerG.append('circle')
.attr('class', 'marker-bg')
.attr('cx', radius)
.attr('cy', radius)
.attr('r', radius);
markerG.append('text')
.attr('x', radius)
.attr('y', radius*0.9)
.text(marker.type);
markerG.append('text')
.attr('x', radius)
.attr('y', radius*1.5)
.text(marker.version);
}
function startTransitions (svg, chartWidth, chartHeight, rectClip, markers, x) {
rectClip.transition()
.duration(1000*markers.length)
.attr('width', chartWidth);
markers.forEach(function (marker, i) {
setTimeout(function () {
addMarker(marker, svg, chartHeight, x);
}, 1000 + 500*i);
});
}
function makeChart (data, markers) {
var svgWidth = 960,
svgHeight = 500,
margin = { top: 20, right: 20, bottom: 40, left: 40 },
chartWidth = svgWidth - margin.left - margin.right,
chartHeight = svgHeight - margin.top - margin.bottom;
var x = d3.time.scale().range([0, chartWidth])
.domain(d3.extent(data, function (d) { return d.date; })),
y = d3.scale.linear().range([chartHeight, 0])
.domain([0, d3.max(data, function (d) { return d.pct95; })]);
var xAxis = d3.svg.axis().scale(x).orient('bottom')
.innerTickSize(-chartHeight).outerTickSize(0).tickPadding(10),
yAxis = d3.svg.axis().scale(y).orient('left')
.innerTickSize(-chartWidth).outerTickSize(0).tickPadding(10);
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// clipping to start chart hidden and slide it in later
var rectClip = svg.append('clipPath')
.attr('id', 'rect-clip')
.append('rect')
.attr('width', 0)
.attr('height', chartHeight);
addAxesAndLegend(svg, xAxis, yAxis, margin, chartWidth, chartHeight);
drawPaths(svg, data, x, y);
startTransitions(svg, chartWidth, chartHeight, rectClip, markers, x);
}
var parseDate = d3.time.format('%Y-%m-%d').parse;
d3.json('data.json', function (error, rawData) {
if (error) {
console.error(error);
return;
}
var data = rawData.map(function (d) {
return {
date: parseDate(d.date),
pct05: d.pct05 / 1000,
pct25: d.pct25 / 1000,
pct50: d.pct50 / 1000,
pct75: d.pct75 / 1000,
pct95: d.pct95 / 1000
};
});
d3.json('markers.json', function (error, markerData) {
if (error) {
console.error(error);
return;
}
var markers = markerData.map(function (marker) {
return {
date: parseDate(marker.date),
type: marker.type,
version: marker.version
};
});
makeChart(data, markers);
});
});
[
{
"date": "2014-08-01",
"pct05": 5350,
"pct25": 6756,
"pct50": 7819,
"pct75": 9284,
"pct95": 13835
},
{
"date": "2014-08-02",
"pct05": 4439,
"pct25": 5584,
"pct50": 6554,
"pct75": 8016,
"pct95": 12765
},
{
"date": "2014-08-03",
"pct05": 4247,
"pct25": 5419,
"pct50": 6332,
"pct75": 7754,
"pct95": 12236
},
{
"date": "2014-08-04",
"pct05": 3293,
"pct25": 4414,
"pct50": 5191,
"pct75": 6491,
"pct95": 10325
},
{
"date": "2014-08-05",
"pct05": 3942,
"pct25": 5134,
"pct50": 6069,
"pct75": 7501,
"pct95": 11685
},
{
"date": "2014-08-06",
"pct05": 2744,
"pct25": 5508,
"pct50": 6879,
"pct75": 9221,
"pct95": 17239
},
{
"date": "2014-08-07",
"pct05": 1807,
"pct25": 3019,
"pct50": 4119,
"pct75": 5656,
"pct95": 8851
},
{
"date": "2014-08-08",
"pct05": 1855,
"pct25": 3386,
"pct50": 4473,
"pct75": 5915,
"pct95": 10580
},
{
"date": "2014-08-09",
"pct05": 1830,
"pct25": 3202,
"pct50": 4233,
"pct75": 5559,
"pct95": 8930
},
{
"date": "2014-08-10",
"pct05": 1828,
"pct25": 3195,
"pct50": 4304,
"pct75": 5482,
"pct95": 9189
},
{
"date": "2014-08-11",
"pct05": 2246,
"pct25": 3929,
"pct50": 5326,
"pct75": 7077,
"pct95": 11648
},
{
"date": "2014-08-12",
"pct05": 2051,
"pct25": 3662,
"pct50": 4849,
"pct75": 6194,
"pct95": 10078
},
{
"date": "2014-08-13",
"pct05": 1700,
"pct25": 3075,
"pct50": 4068,
"pct75": 5259,
"pct95": 9789
},
{
"date": "2014-08-14",
"pct05": 2161,
"pct25": 3891,
"pct50": 5262,
"pct75": 6924,
"pct95": 11612
},
{
"date": "2014-08-15",
"pct05": 1765,
"pct25": 3190,
"pct50": 4388,
"pct75": 5822,
"pct95": 9433
},
{
"date": "2014-08-16",
"pct05": 2036,
"pct25": 3756,
"pct50": 4775,
"pct75": 6158,
"pct95": 9999
},
{
"date": "2014-08-17",
"pct05": 2079,
"pct25": 3561,
"pct50": 4753,
"pct75": 6124,
"pct95": 9807
},
{
"date": "2014-08-18",
"pct05": 2108,
"pct25": 3576,
"pct50": 4818,
"pct75": 6344,
"pct95": 10235
},
{
"date": "2014-08-19",
"pct05": 2143,
"pct25": 3792,
"pct50": 5073,
"pct75": 6772,
"pct95": 11338
},
{
"date": "2014-08-20",
"pct05": 2086,
"pct25": 3801,
"pct50": 5073,
"pct75": 6688,
"pct95": 12394
},
{
"date": "2014-08-21",
"pct05": 1767,
"pct25": 3253,
"pct50": 4282,
"pct75": 5563,
"pct95": 9167
},
{
"date": "2014-08-22",
"pct05": 1756,
"pct25": 3047,
"pct50": 3950,
"pct75": 5006,
"pct95": 7948
},
{
"date": "2014-08-23",
"pct05": 2123,
"pct25": 3755,
"pct50": 5173,
"pct75": 7243,
"pct95": 12338
},
{
"date": "2014-08-24",
"pct05": 1967,
"pct25": 3404,
"pct50": 4529,
"pct75": 5970,
"pct95": 9897
},
{
"date": "2014-08-25",
"pct05": 1537,
"pct25": 2612,
"pct50": 3394,
"pct75": 4279,
"pct95": 7104
},
{
"date": "2014-08-26",
"pct05": 2182,
"pct25": 3958,
"pct50": 5505,
"pct75": 7642,
"pct95": 12707
},
{
"date": "2014-08-27",
"pct05": 1932,
"pct25": 3366,
"pct50": 4526,
"pct75": 6086,
"pct95": 9930
},
{
"date": "2014-08-28",
"pct05": 1268,
"pct25": 2344,
"pct50": 3256,
"pct75": 4215,
"pct95": 6673
},
{
"date": "2014-08-29",
"pct05": 1225,
"pct25": 2239,
"pct50": 3033,
"pct75": 4111,
"pct95": 7601
},
{
"date": "2014-08-30",
"pct05": 1393,
"pct25": 2432,
"pct50": 3417,
"pct75": 4710,
"pct95": 8798
},
{
"date": "2014-08-31",
"pct05": 1175,
"pct25": 2020,
"pct50": 2768,
"pct75": 3889,
"pct95": 7744
},
{
"date": "2014-09-01",
"pct05": 989,
"pct25": 1655,
"pct50": 2218,
"pct75": 3167,
"pct95": 6018
},
{
"date": "2014-09-02",
"pct05": 1249,
"pct25": 2069,
"pct50": 2738,
"pct75": 3938,
"pct95": 7574
},
{
"date": "2014-09-03",
"pct05": 936,
"pct25": 1510,
"pct50": 1968,
"pct75": 2700,
"pct95": 5215
},
{
"date": "2014-09-04",
"pct05": 1264,
"pct25": 2039,
"pct50": 2657,
"pct75": 3646,
"pct95": 7042
},
{
"date": "2014-09-05",
"pct05": 1305,
"pct25": 2106,
"pct50": 2745,
"pct75": 3766,
"pct95": 7273
},
{
"date": "2014-09-06",
"pct05": 798,
"pct25": 1288,
"pct50": 1678,
"pct75": 2303,
"pct95": 4448
},
{
"date": "2014-09-07",
"pct05": 1314,
"pct25": 2120,
"pct50": 2763,
"pct75": 3791,
"pct95": 7321
},
{
"date": "2014-09-08",
"pct05": 1042,
"pct25": 1681,
"pct50": 2191,
"pct75": 3007,
"pct95": 5806
}
]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Trend Chart (Area + Line)</title>
<link rel="stylesheet" href="app.css">
</head>
<body>
</body>
<script src="http://d3js.org/d3.v3.js"></script>
<script src="app.js"></script>
</html>
Copyright (c) 2014 Ross Kirsling
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[
{
"date": "2014-08-06",
"type": "Client",
"version": "2.0"
},
{
"date": "2014-08-20",
"type": "Client",
"version": "2.1"
},
{
"date": "2014-08-27",
"type": "Server",
"version": "3.5"
},
{
"date": "2014-09-03",
"type": "Client",
"version": "2.2"
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment