Public
Edited
Aug 30, 2023
2 forks
15 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
// Chart dimension
const margin = { top: 55, right: 125, bottom: 25, left: 125 };
// const width = 1110;
const height = 760;

// Colors for the bubbles, one per 'highway' value
const color = d3.scaleOrdinal()
.range(['#DB7F85', '#50AB84', '#4C6C86', '#C47DCB', '#B59248', '#DD6CA7', '#E15E5A', '#5DA5B3', '#725D82', '#54AF52', '#954D56', '#8C92E8', '#D8597D', '#AB9C27', '#D67D4B', '#D58323', '#BA89AD', '#357468', '#8F86C2', '#7D9E33', '#517C3F', '#9D5130', '#5E9ACF', '#776327', '#944F7E']);

const name = nameToKey[area] || Object.keys(datasets)[0];
const dataset = datasets[name];

// Remove previous chart if any
d3.selectAll('.plot > svg').remove();

// Create chart
const svg = d3.select('.plot').append('svg')
.style('width', `${width}px`)
.style('height', `${height}px`);

// Data to be plotted and for the axis
const data = dataset.data;
const all_keys = Array.from(dataset.all_keys);
const years = dataset.years;
const minYear = years[0];
const maxYear = years[years.length - 1];
const nb_keys = all_keys.size;

// Scales
const xscale = d3.scaleLinear()
.domain([minYear, maxYear])
.range([margin.left, width - margin.right]);

const yscale = d3.scalePoint()
.domain(Object.keys(data[maxYear]).map((_, i) => i + 1))
.rangeRound([margin.top, height - margin.bottom]);

// Axis
const xaxis = d3.axisBottom()
.scale(xscale)
.tickFormat(d3.format(''));

const xaxis2 = d3.axisTop()
.scale(xscale)
.tickFormat(d3.format(''));

svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + (height - margin.bottom) + ')')
.call(xaxis);

svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + (margin.top - 30) + ')')
.call(xaxis2);

const orderMinYear = Object.keys(data[minYear])
.map(t => [t, data[minYear][t].rank])
.sort((a, b) => a[1] - b[1])
.map(d => d[0]);

const orderMaxYear = Object.keys(data[maxYear])
.map(t => [t, data[maxYear][t].rank])
.sort((a, b) => a[1] - b[1])
.map(d => d[0]);

const visiblesMinYear = Object.keys(data[minYear])
.map(t => [t, data[minYear][t].percent])
.filter(a => a[1] && +a[1] >= 0.9)
.map(a => a[0]);

const visiblesMaxYear = Object.keys(data[maxYear])
.map(t => [t, data[maxYear][t].percent])
.filter(a => a[1] && a[1] >= 0.9)
.map(a => a[0]);

const firstYearVisible = {};
const lastYearVisible = {};
years.forEach((y) => {
all_keys.forEach((k) => {
if (data[y][k].percent >= 0.9 && !firstYearVisible[k]) {
firstYearVisible[k] = y;
}
if (data[y][k].percent >= 0.9) {
lastYearVisible[k] = y;
}
});
});
all_keys.forEach((k) => {
if (firstYearVisible[k] === years[0]) {
firstYearVisible[k] = null;
}
if (lastYearVisible[k] === years[years.length - 1] || lastYearVisible[k] === years[0]) {
lastYearVisible[k] = null;
}
});

const yAxisLeft = svg.append('g')
.attr('class', 'y axis yleft')
.attr('transform', 'translate(' + [margin.left - 30, 0]+ ')')
.call(d3.axisLeft()
.scale(d3.scalePoint()
.domain(orderMinYear)
.rangeRound([margin.top, height - margin.bottom])
));

yAxisLeft.selectAll('text')
.each(function (d) {
const k = this.innerHTML;
if (firstYearVisible[k]) {
const y = firstYearVisible[k];
const tx = xscale(y);
const ty = yscale(data[y][k].rank) - this.parentElement.transform.baseVal.getItem(0).matrix.f;
this.setAttribute('transform', `translate(${[tx - margin.left, ty]})`);
}
});

const yAxisRight = svg.append('g')
.attr('class', 'y axis yright')
.attr('transform', 'translate(' + [width - margin.left + 30, 0]+ ')')
.call(d3.axisRight()
.scale(d3.scalePoint()
.domain(orderMaxYear)
.rangeRound([margin.top, height - margin.bottom])
));

yAxisRight.selectAll('text')
.each(function (d) {
const k = this.innerHTML;
if (lastYearVisible[k]) {
const y = lastYearVisible[k];
const tx = width - xscale(y) - margin.left;
const ty = yscale(data[y][k].rank) - this.parentElement.transform.baseVal.getItem(0).matrix.f;
this.setAttribute('transform', `translate(${[-tx, ty]})`);
}
});

// Reset state of axis visibility to default
const resetVisibilityAxis = () => {
yAxisLeft
.selectAll('text')
.style('opacity', 1)
.style('font-weight', 400)
.style('fill', function (d) {
return visiblesMinYear.indexOf(this.innerHTML) > -1 ? 'black' : 'transparent';
});

yAxisRight
.selectAll('text')
.style('opacity', 1)
.style('font-weight', 400)
.style('fill', function (d) {
return visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 'black' : 'transparent';
});
};

// Highlight a specific entry on the axis
const highlightAxis = (name) => {
// svg.select('.yleft')
// .selectAll('text')
// .style('fill', function() { return name === this.innerHTML ? 'black' : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 'black' : 'transparent'; })
// .style('opacity', function() { return name === this.innerHTML ? 1 : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 0.3 : 1; })
// .style('font-weight', function() { return name === this.innerHTML ? 400 : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 400 : 400; });

svg.select('.yleft')
.selectAll('text')
.style('fill', function() { return name === this.innerHTML && visiblesMaxYear.indexOf(name) > -1 ? 'black' : 'transparent'; })
.style('opacity', function() { return name === this.innerHTML ? 1 : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 0.3 : 1; })
.style('font-weight', function() { return name === this.innerHTML ? 400 : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 400 : 400; });

svg.select('.yright')
.selectAll('text')
.style('fill', function() { return name === this.innerHTML ? 'black' : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 'black' : 'transparent'; })
.style('opacity', function() { return name === this.innerHTML ? 1 : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 0.3 : 1; })
.style('font-weight', function() { return name === this.innerHTML ? 800 : visiblesMaxYear.indexOf(this.innerHTML) > -1 ? 400 : 400; });
};
resetVisibilityAxis();

// Add the lines
const group_tag = svg.selectAll('.tagline')
.data(all_keys)
.enter()
.append('g')
.attr('class', d => `tagline g_${d}`);

const line = d3.line()
.x(d => xscale(d[0]))
.y(d => yscale(d[1]))
.curve(d3.curveCatmullRom.alpha(0.66));

const radius = d3.scalePow().exponent(0.5)
.domain([0, 100])
.range([1, (width - margin.left - margin.right) / (years.length * 2.2)]);

group_tag.append('path')
.datum(d => d)
.attr('fill', 'none')
.attr('stroke', (d) => color(d))
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round')
.attr('stroke-width', '1.75px')
.attr('d', (d) => splitArray(years.map(y => data[y][d].percent > 0 ? [y, data[y][d].rank] : null), null)
.filter(d => d.length > 1)
.map(d => line(d)));

// Add the bubbles
const g_circle = group_tag.append('g')
.selectAll('circle')
.data(years)
.enter();
g_circle.append('circle')
.attr('r', function(d) { return radius(data[d][this.parentElement.__data__].percent); })
.attr('cx', (d) => xscale(d))
.attr('cy', function(d) { return yscale(data[d][this.parentElement.__data__].rank); })
.attr('fill', function(d) { return color(this.parentElement.__data__); })

// Big transparent circle under each bubble to ease the mouseover on small bubbles
g_circle.append('circle')
.attr('r', 25)
.attr('fill', 'transparent')
.attr('cx', d => xscale(d))
.attr('cy', function (d) {
const tag = this.parentElement.__data__;
const v = data[d][tag];
return yscale(v.rank);
})
.on('mouseover', function (event, d) {
const tag = this.parentElement.__data__;
if (data[d][tag].percent === 0) return;
svg.selectAll(`._${d}.${tag}`).style('display', null);
svg.selectAll(`.tagline:not(.g_${tag})`).style('opacity', 0.3);
highlightAxis(tag);
})
.on('mouseout', function (d) {
svg.selectAll('.tooltip').style('display', 'none');
svg.selectAll(`.tagline`).style('opacity', 'initial');
resetVisibilityAxis();
});
// .on('click', function (d) {
// const tag = this.parentElement.__data__;
// highlightAxis(tag);
// svg.selectAll(`.tooltip.${tag}`).style('display', null);
// svg.selectAll(`.tagline:not(.g_${tag})`).style('opacity', 0.3);
// });

// Add all the labels on the bubbles now
// and set them to be undisplayed
group_tag.each(function (tag) {
g_circle.each(function (d) {
const v = data[d][tag];
if (v.percent !== 0) {
svg.append('text')
.attr('x', xscale(d))
.attr('y', yscale(v.rank) - 7)
.attr('class', `tooltip _${d} ${tag}`)
.attr('fill', 'black')
.attr('font-size', 14)
.attr('text-anchor', 'middle')
.attr('pointer-events', 'none')
.style('display', 'none')
.text(`${tag} - ${d}:`);
svg.append('text')
.attr('x', xscale(d))
.attr('y', yscale(v.rank) + 7)
.attr('class', `tooltip _${d} ${tag}`)
.attr('fill', 'black')
.attr('font-size', 14)
.attr('text-anchor', 'middle')
.attr('pointer-events', 'none')
.style('display', 'none')
.text(`${roundValue(v.percent, 1)}% / rank: ${v.rank}`);
}
});
});

}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more