Built with blockbuilder.org
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rough.js/3.1.0/rough.js"></script>
<style>
* {
box-sizing: border-box;
}
body {
margin:0;position:fixed;top:0;right:0;
bottom:0;left:0;padding: 20px;
}
#container {
border: 1px solid #fabcc3;
background: #fde3e6;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: stretch;
}
#left {
order: 0;
display: flex;
align-items: center;
}
#main {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
#right {
text-align: center;
padding: 12px 4px 0 8px;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 90px;
}
#right text {
font-family: verdana;
fill-opacity: 0.7;
text-anchor: middle;
font-size: 0.8em;
}
#right #legend > * {
display: flex;
justify-content: space-between;
padding: 4px 8px;
}
#yaxisLegend {
position: relative;
transform: rotate(-90deg) translateX(-200%);
transform-origin: center center;
width: 1em;
margin: 0px 4px
}
#bottom {
width: 100%;
padding: 4px;
}
#top {
text-align: center;
background-color: #fabcc3;
padding: 4px 25px;
width: 100%;
}
.lollipop line, .lollipop circle {
fill: #a11014;
stroke: #a11014;
stroke-width: 2;
}
.tick line {
stroke-opacity: 0.2;
stroke-dasharray: 5;
}
.tick text {
fill-opacity: 0.7;
stroke-opacity: 0.7;
font-family: verdana;
}
#xaxisLegend, #yaxisLegend, #circleLegend{
font-family: verdana;
opacity: 0.7;
}
#xaxisLegend {
text-align: center;
}
#circleLegend {
text-align: center;
text-decoration: underline;
word-wrap: break-word;
}
#title {
text-transform: uppercase;
font-family: verdana;
text-align: center;
}
#subTitle {
font-family: verdana;
font-style: italic;
opacity: 0.5;
}
</style>
</head>
<body>
<div id="container">
<div id="top">
<div id="title"></div>
<div id="subTitle"></div>
</div>
<div id="left"><div id="yaxisLegend">yaxis</div></div>
<div id="main">
<div id="bottom"><div id="xaxisLegend">xaxis</div></div></div>
<div id="right">
<div id="circleLegend">circle</div>
<div id="legend"></div>
</div>
</div>
<script>
const dataConfig = {
heightMultiplier: 0.75,
title: 'Vem tjänar mest i Allsvenskan?',
subTitle: 'Erfarenhet är inte det avgörande',
yAxis: {accessor: 'pay', title: 'Inkomst kr', ticks: 6 },
xAxis: {accessor: 'year', title: 'År', rotation: -45 },
circle: {accessor: 'experience', title: 'Ålder', steps:3, maxSize: 30},
sortAttribute: 'year',
sortDirection: 'descending',
}
let data = [
{ pay: 40000, experience: 24, year: '2008' },
{ pay: 30000, experience: 5, year: '2009' },
{ pay: 50000, experience: 7, year: '2010' },
{ pay: 60000, experience: 10, year: '2011' },
{ pay: 20000, experience: 3, year: '2012' },
{ pay: 80000, experience: 1, year: '2013' },
{ pay: 90000, experience: 4, year: '2014' },
{ pay: 50000, experience: 12, year: '2015' },
{ pay: 60000, experience: 5, year: '2016' },
{ pay: 30000, experience: 7, year: '2017' },
{ pay: 10000, experience: 6, year: '2018' },
];
// sort data
if(dataConfig.sortAttribute && dataConfig.sortDirection) {
data.sort( (a,b) => d3[dataConfig.sortDirection](a[dataConfig.sortAttribute], b[dataConfig.sortAttribute]) )
}
const config = {
containerWidth: 500,
bottomMargin: 40,
topMargin: 30,
rightMargin: 20,
leftMargin: 60,
yaxisOffset: -10,
legendTextOffset: 4,
}
const circleScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d[dataConfig.circle.accessor])])
.range([1, dataConfig.circle.maxSize]);
function drawData() {
/* CONTAINERS AND INSTANTIATION */
config.width = d3.select('#main').node().getBoundingClientRect().width;
config.height = config.width * dataConfig.heightMultiplier;
const svg = d3.select("#main").append("svg").lower()
.attr("height", config.height)
.attr("width", config.width);
const rc = rough.svg(svg);
/* SCALES */
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d[dataConfig.yAxis.accessor])])
.range([config.height - config.bottomMargin - config.topMargin, 0]);
const xScale = d3.scalePoint()
.domain(data.map(d => d[dataConfig.xAxis.accessor]))
.range([config.width - config.rightMargin - config.leftMargin, 0]);
/* AXIS */
const x_axis = d3.axisBottom()
.scale(xScale)
const xAxis = svg.append("g").classed("xaxis", true)
.attr("transform", `translate(${config.leftMargin} ${config.height - config.bottomMargin})`)
.call(x_axis);
if (dataConfig.xAxis.rotation) {
xAxis
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", `rotate(${dataConfig.xAxis.rotation})`);
}
xAxis.selectAll(".tick line").attr("y1", config.topMargin + config.bottomMargin - config.height );
const y_axis = d3.axisLeft()
.scale(yScale);
if(dataConfig.yAxis.ticks) {
y_axis.ticks(dataConfig.yAxis.ticks);
}
const yAxis = svg.append("g").classed("yaxis", true)
.attr("transform", `translate(${config.leftMargin + config.yaxisOffset} ${config.topMargin})`)
.call(y_axis);
yAxis.select(".domain").remove();
yAxis.selectAll(".tick:not(:first-of-type) line").attr("x1", config.width);
yAxis.select('.tick:first-child').remove();
/* DATA */
const field = svg
.append("g")
.attr("transform", `translate(${config.leftMargin} ${config.topMargin})`);
const lollipop = field.selectAll("g.lollipop")
.data(data)
.enter()
.append("g")
.classed("lollipop", true);
lollipop.each(function(d) {
//centerX, centerY, diameter
const circle = rc.circle(
xScale(d[dataConfig.xAxis.accessor]),
yScale(d[dataConfig.yAxis.accessor]),
circleScale(d[dataConfig.circle.accessor]),
{
roughness: 0.7,
fill: '#a11014',
stroke: '#a11014',
fillStyle: 'cross-hatch',
fillWeight: 1
}
);
// x1, y1, x2, y2
const line = rc.line(
xScale(d[dataConfig.xAxis.accessor]),
yScale.range()[0],
xScale(d[dataConfig.xAxis.accessor]),
yScale(d[dataConfig.yAxis.accessor]) + circleScale(d[dataConfig.circle.accessor]) - 10,
{
roughness: 2,
stroke: '#a11014'
}
);
this.append(circle);
this.append(line);
});
}
function drawLegend() {
const container = d3.select('#right')
const containerBCR = container.node().getBoundingClientRect();
const valuesToShowGenerator = n => {
const domain = circleScale.domain();
if (n < 0) return [];
if (n === 1) return [domain[1]];
if (n === 2) return [domain[0], domain[1]];
const domainLength = domain[1]-domain[0];
const domainUnit = domainLength/n;
const result = Array.apply(null, {length: n}).map((e, i, arr) => {
return domainUnit * (i+1);
});
return result;
}
const valuesToShow = valuesToShowGenerator(
dataConfig.circle.steps
).reverse();
const maxCircleWidth = circleScale(valuesToShow[0]);
const legendElement = d3.select("#right #legend")
.selectAll("div")
.data(valuesToShow)
.enter()
.append("div")
legendElement.append("div").each(function(d) {
const svg = d3.select(this).append("svg")
.attr("width", maxCircleWidth)
.attr("height", circleScale(d));
const rc = rough.svg(svg);
const circle = rc.circle(
maxCircleWidth/2,
circleScale(d)/2,
circleScale(d),
{
roughness: 0.7,
fill: '#a11014',
stroke: '#a11014',
fillStyle: 'cross-hatch',
fillWeight: 1
});
svg.node().appendChild(circle);
});
legendElement.append("div")
.text(d => d.toFixed(0));
}
function setup() {
d3.select('#container').node().style.width = `${config.containerWidth}px`;
/* LEGEND */
const keyNames = Object.keys(data[0]);
d3.select("div#xaxisLegend").node().innerText = dataConfig.xAxis.title;
d3.select("div#yaxisLegend").node().innerHTML = dataConfig.yAxis.title;
d3.select("div#circleLegend").node().innerHTML = dataConfig.circle.title;
d3.select("div#title").node().innerText = dataConfig.title;
if (dataConfig.subTitle) {
d3.select("div#subTitle").node().innerText = dataConfig.subTitle;
}
}
setup();
drawLegend();
drawData();
</script>
</body>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/rough.js/3.1.0/rough.js