Public
Edited
Nov 23, 2023
Insert cell
Insert cell
Insert cell
Insert cell
{
let view = html`<div class="radarChart" style="display: inline-flex;"></div>`;
yield view;
// Define the data for the radar chart
let HKData = {
name: "Hong Kong",
axes: [
{axis: 'avgGDPIndex', value: 100},
{axis: 'lifeExpectancy', value: 85.29},
{axis: 'employmentRate', value: 95.7},
{axis: 'urbanPopulationRatio', value: 100},
{axis: 'latestLiteracyRate', value: 95.7},
{axis: 'absLatitude', value: 22.3193},
{axis: 'laborForceParticipation', value: 61.2},
{axis: 'infantMortality', value: 1.169},
{axis: 'fertilityRateT', value: 14.13},
{axis: 'healthExpenditureRatio', value: 30}
],
color: '#26AF32'
};
let radarOptions = {
w: 600,
h: 200,
margin: margin,
maxValue: 100,
levels: 10,
roundStrokes: false,
//color: d3.scaleOrdinal().range(["#f44336","#e81e63","#9c27b0", "#673ab7", "#3f51b5", "#2196f3","#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722"]),
format: '.0f',
legend: {title: "Stats", translateX: 0, translateY: 0},
unit: "%"
};
const chartData = [...radarData, HKData];
// Define the options for the radar chart
let container = d3.select(view);
// Call the radar chart function to generate the chart
return RadarChart(container, chartData, radarOptions);
}
Insert cell
attributes = ['avgGDPIndex', 'lifeExpectancy', 'employmentRate', 'urbanPopulationRatio', 'latestLiteracyRate', 'absLatitude', 'laborForceParticipation', 'infantMortality', 'fertilityRateT', 'healthExpenditureRatio'];
Insert cell
radarData = selectedCountries.map((country) => {
const properties = country.properties;
return {
name: properties.name,
axes: attributes.map((attribute) => {
return {
axis: attribute,
value: properties[attribute]
}
}),
color: d3.scaleOrdinal(d3.schemeCategory10)
}
});
Insert cell
radarData[1]
Insert cell
Insert cell
height = width * 0.7
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
svg = d3
.create('svg')
.style('width', width)
.style('height', height);
Insert cell
selectedLayer = svg.append('g').attr('class', 'selected-countries');
Insert cell
layer = svg.append('g').attr('class', 'counties');
Insert cell
Insert cell
countries = topojson.feature(world, world.objects.countries).features
Insert cell
boundaries = topojson.mesh(world,world.objects.countries, (a, b) => a !== b);
Insert cell
Insert cell
paletteScale = d3
.scaleLinear()
.domain([minValue, maxValue])
.range(['#EFEFFF', '#CF4646']);
Insert cell
Insert cell
geoData = countries.map(country => {
const countryData = data.get(country.id);
const densePopulation = countryData === undefined ? 0 : countryData["Population Density"];
const totalPopulation = countryData === undefined ? 0 : countryData["Population"];
const GDP = countryData === undefined ? 0 : countryData["GDP"];
const avgGDP = GDP/totalPopulation
const avgGDPIndex = avgGDP/49146*100
const lifeExpectancy = countryData === undefined ? 0 : countryData["Life Expectancy"];
const employmentRate = countryData === undefined ? 0 : countryData["Employment Rate"];
const urbanPopulationRatio = countryData === undefined ? 0 : countryData["Urban Population Ratio"];
const latestLiteracyRate = countryData === undefined ? 0 : countryData["Latest Literacy Rate"];
const absLatitude = countryData === undefined ? 0 : countryData["ABS Latitude"];
const laborForceParticipation = countryData === undefined ? 0 : countryData["Labor Force Participation"];
const infantMortality = countryData === undefined ? 0 : countryData["Infant Mortality"];
const fertilityRateT = countryData === undefined ? 0 : countryData["Fertility Rate"]*10;
const healthExpenditureRatio = countryData === undefined ? 0 : countryData["Out of Pocket Health Expenditure"];
return {
...country,
properties: {
totalPopulation: totalPopulation,
densePopulation: densePopulation,
fillColor: paletteScale(totalPopulation),
name: countryData === undefined ? "" : countryData['Country'],
GDP: GDP,
lifeExpectancy: lifeExpectancy,
employmentRate: employmentRate,
urbanPopulationRatio: urbanPopulationRatio,
latestLiteracyRate: latestLiteracyRate,
avgGDP: avgGDP,
avgGDPIndex: avgGDPIndex,
absLatitude: absLatitude,
laborForceParticipation: laborForceParticipation,
infantMortality: infantMortality,
fertilityRateT: fertilityRateT,
healthExpenditureRatio: healthExpenditureRatio
},
}
})
Insert cell
Insert cell
projection = d3
.geoMercator()
.center([4.8357, 45.764]) // this is centered on France
.scale(200)
.translate([width / 2, height / 2]); // The map will appear at the right spot
Insert cell
path = d3.geoPath().projection(projection);
Insert cell
zoom = {
const zoomed = ({ transform }) => {
layer.attr('transform', transform);
selectedLayer.attr('transform', transform);
};
return d3.zoom().scaleExtent([0.5, 40]).on('zoom', zoomed);
}
Insert cell
svg.call(zoom);
Insert cell
Insert cell
countriesSelection = {
layer
.selectAll('path')
.data(geoData, (d) => d.id)
.join(
enter => {
enter
.append('path')
.attr('class', (d) => `country ${d.id}`)
.attr('d', path)
.style('fill', (d) => d.properties.fillColor)
.style('stroke', 'none');
},
() => {},
exit => {
exit
.remove();
},
);
return layer.selectAll('.country');
}
Insert cell
Insert cell
boundariesLayer = {
countriesSelection
layer
.selectAll('.country-boundary')
.data([boundaries])
.join(
enter => {
enter
.append('path')
.attr('d', path)
.attr('class', 'country-boundary')
.style('stroke', 'black')
.style('stroke-width', 1)
.style('stroke-opacity', 0.3)
.style('fill', 'none');
},
() => {},
exit => {
exit.remove()
}
);
return layer.selectAll('.country-boundary');
}
Insert cell
Insert cell
tooltipSelection = d3.select('body')
.append('div')
.attr('class', 'hover-info')
.style('visibility', 'hidden');
Insert cell
Insert cell
tooltipEventListeners = countriesSelection
.on('mouseenter', ({ target }) => {
tooltipSelection.style('visibility', 'visible');

d3.select(target)
.transition()
.duration(150)
.ease(d3.easeCubic)
.style('fill', '#ffba08');
})
.on('mousemove', ({ pageX, pageY, target }) => {

tooltipSelection
.style('top', `${pageY + 20}px`)
.style('left', `${pageX - 10}px`)
.style('z-index', 100)
.html(
`<strong>${
target.__data__.properties.name
}</strong><br>Total population: <strong>${
target.__data__.properties.totalPopulation
}</strong><br>Life Expectancy: <strong>${
target.__data__.properties.lifeExpectancy
}</strong><br>Employment Rate: <strong>${
target.__data__.properties.employmentRate
}</strong><br>Urban Ratio: <strong>${
target.__data__.properties.urbanPopulationRatio
}</strong><br>Literacy Rate: <strong>${
target.__data__.properties.latestLiteracyRate
}</strong><br>GDP: <strong>${
target.__data__.properties.GDP
}</strong><br>Avg GDP: <strong>${
target.__data__.properties.avgGDP
}</strong><br>Avg GDP Index(Div by HK): <strong>${
target.__data__.properties.avgGDPIndex
}</strong><br>ABS Latitude: <strong>${
target.__data__.properties.absLatitude
}</strong><br>Labor Force Participation: <strong>${
target.__data__.properties.laborForceParticipation
}</strong><br>Infant Mortality: <strong>${
target.__data__.properties.infantMortality
}</strong><br>Fertility Rate(*10): <strong>${
target.__data__.properties.fertilityRateT
}</strong><br>Out of Pocket Health Expenditure: <strong>${
target.__data__.properties.healthExpenditureRatio
}</strong>`,
)
.append('div')
.attr('class', 'triangle');

d3.selectAll('.triangle').style('top', `${-Math.sqrt(20) - 3}px`);
})
.on('mouseleave', (e) => {
tooltipSelection.style('visibility', 'hidden');

d3.select(e.target)
.transition()
.duration(150)
.ease(d3.easeCubic)
.style('fill', (d) => d.properties.fillColor);
});

Insert cell
Insert cell
buttn = d3.create('button').html('<h3>Reinitialize Selection</h3>');
Insert cell
Insert cell
selectedCountriesSet = {
countriesSelection
return Generators.observe(next => {
let selectedSet = new Set()
// Yield the initial value.
next(selectedSet);
// Define event listeners to yield the next values
svg.selectAll('.country')
.on('click', null)
.on('click', ({ target }) => {
selectedSet.add(target.__data__.id);
next(selectedSet)
});
// Define the event listener of the button
buttn.on('click', () => {
selectedSet = new Set();
next(selectedSet);
});
// When the generator is disposed, detach the event listener.
return () => svg.selectAll('.country').on('click', null);
});
}
Insert cell
Insert cell
selectedCountries = geoData.filter((country) => selectedCountriesSet.has(country.id));

Insert cell
selectedCountries
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
typeof selectedCountries
Insert cell
Insert cell
totalPopulationSelection = selectedCountries.reduce((acc, country) => acc + country.properties.totalPopulation, 0);
Insert cell
Insert cell
geo = selectedCountries.map(country => {
const newCountry = {
...country,
properties: {
...country.properties,
totalPopulation: totalPopulationSelection,
name: `${country.properties.name} (Selected)`,
}
};
return newCountry;
});
Insert cell
Insert cell
t = function getTransition() {
return d3.transition().duration(250).ease(d3.easeCubic);
}

Insert cell
Insert cell
changeSelectedLayer = {
const currentTransition = t();
selectedLayer
.selectAll('path')
.data(geo, (d) => d.id)
.join(
(enter) => {
enter
.append('path')
.attr('class', (d) => `selected-country ${d.id}`)
.attr('d', path)
.style('fill', '#ffba08')
.style('fill', '#f4a261')
.style('stroke', 'black')
.style('stroke-width', 1)
.style('stroke-opacity', 0.3)
.call((en) =>
en
.transition(currentTransition)
.style('fill', '#f4a261')
.style('stroke-opacity', 0.1),
);
},
() => {},
(exit) => {
exit.style('fill', '#f4a261').call((ex) =>
ex
.transition(currentTransition)
.style('fill', (d) => d.properties.fillColor)
.remove(),
);
},
);
}
Insert cell
Insert cell
{
changeSelectedLayer
selectedLayer
.on('mouseenter', ({ target }) => {
tooltipSelection.style('visibility', 'visible');

d3.select(target).style('fill', '#ffba08');
})
.on('mousemove', ({ pageX, pageY, target }) => {
const x = pageX;
const y = pageY;

tooltipSelection
.html(
`<strong>${
target.__data__.properties.name
}</strong><br>Total population of the selection:<strong>${target.__data__.properties.totalPopulation}</strong>`,
)
.style('top', `${y + 20}px`)
.style('left', `${x - 10}px`)

d3.selectAll('.triangle').style('top', `${-Math.sqrt(20) - 3}px`);
})
.on('mouseleave', ({ target }) => {
tooltipSelection.style('visibility', 'hidden');

d3.select(target).style('fill', '#f4a261');
});
}
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@6')
Insert cell
import { DailyOptionStats } from '@stroked/hod-daily-quote'
Insert cell
import { turboColorScheme } from '@stroked/how-to-slice-a-sphere'
Insert cell
topojson = require('topojson-client@3')
Insert cell
md`## Data fetching`
Insert cell
world = d3.json('https://gist.githubusercontent.com/olemi/d4fb825df71c2939405e0017e360cd73/raw/d6f9f0e9e8bd33183454250bd8b808953869edd2/world-110m2.json')
Insert cell
rawData = FileAttachment("world-2023-plus.csv").csv()
Insert cell
rawData
Insert cell
countryCodes = d3.tsv('https://d3js.org/world-110m.v1.tsv')
Insert cell
md`## Data formatting`
Insert cell
letterToNum = {
const letToNum = new Map();
countryCodes.forEach(item => {
if (item.iso_a2 !== "-99" && item.iso_n3 !== "-99") {
letToNum.set(item.iso_a2, item.iso_n3);
}
});
return letToNum;
}
Insert cell
data = {
const arrayData = rawData.map(item => {
let newDatum = Object.assign({}, item);
// Str to int
newDatum["Population Density"] = +item["Population Density"].replace(/[$,%]/g, "");
newDatum["Population"] = +item["Population"].replace(/[$,%]/g, "");
newDatum["GDP"] = +item["GDP"].replace(/[$,%]/g, "");
newDatum["Life Expectancy"] = +item["Life Expectancy"].replace(/[$,%]/g, "");
newDatum["Employment Rate"] = +item["Employment Rate"].replace(/[$,%]/g, "");
newDatum["Urban Population Ratio"] = +item["Urban Population Ratio"].replace(/[$,%]/g, "");
newDatum["Latest Literacy Rate"] = +item["Latest Literacy Rate"].replace(/[$,%]/g, "");
newDatum["ABS Latitude"] = +item["ABS Latitude"].replace(/[$,%]/g, "");
newDatum["Labor Force Participation"] = +item["Labor Force Participation"].replace(/[$,%]/g, "");
newDatum["Infant Mortality"] = +item["Infant Mortality"].replace(/[$,%]/g, "");
newDatum["Fertility Rate"] = +item["Fertility Rate"].replace(/[$,%]/g, "");
newDatum["Out of Pocket Health Expenditure"] = +item["Out of Pocket Health Expenditure"].replace(/[$,%]/g, "");
// Get Country code -> original data is number, pad to 3 digits
newDatum["Country Code"] = letterToNum.get(item["Abbreviation"]);
return newDatum;
}).filter(item => {
return item["Country Code"] !== undefined;
})
const data = new Map()
arrayData.forEach(item => {
data.set(+item["Country Code"], item);
})
return data;
}
Insert cell
defaultMinValue = {
let minVal = 100000000000;
data.forEach(value => {
minVal = Math.min(minVal, value["Population"])
})
return minVal;
}
Insert cell
defaultMaxValue = {
let maxVal = 0;
data.forEach(value => {
maxVal = Math.max(maxVal, value["Population"])
})
return maxVal;
}
Insert cell
Insert cell
styles = html`
<style>
.hover-info {
width: 150px;
z-index: 10001;
position: absolute;
background: aliceblue;
border: 2px solid black;
border-radius: 4px;
overflow: visible;
}

.triangle {
position: absolute;
z-index: -1;
width: 10px;
height: 10px;
left: 5px;
transform: rotate(45deg);
background: aliceblue;
border-left: 2px solid black;
border-top: 2px solid black;
}
</style>`
Insert cell
RadarChart = function RadarChart(parent, data, options) {
const wrap = (text, width) => {
text.each(function() {
var text = d3.select(this),
words = text
.text()
.split(/\s+/)
.reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.4, // ems
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text
.text(null)
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", dy + "em");

while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
}; //wrap

const cfg = {
w: 600, //Width of the circle
h: 600, //Height of the circle
margin: { top: 20, right: 20, bottom: 20, left: 20 }, //The margins of the SVG
levels: 3, //How many levels or inner circles should there be drawn
maxValue: 0, //What is the value that the biggest circle will represent
labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
opacityArea: 0.1, //The opacity of the area of the blob
dotRadius: 4, //The size of the colored circles of each blog
opacityCircles: 0.1, //The opacity of the circles of each blob
strokeWidth: 2, //The width of the stroke around each blob
roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
color: d3.scaleOrdinal(d3.schemeCategory10), //Color function,
format: '.2%',
unit: '',
legend: false
};

//Put all of the options into a variable called cfg
if ('undefined' !== typeof options) {
for (var i in options) {
if ('undefined' !== typeof options[i]) {
cfg[i] = options[i];
}
} //for i
} //if

//If the supplied maxValue is smaller than the actual one, replace by the max in the data
// var maxValue = max(cfg.maxValue, d3.max(data, function(i){return d3.max(i.map(function(o){return o.value;}))}));
let maxValue = 0;
for (let j = 0; j < data.length; j++) {
for (let i = 0; i < data[j].axes.length; i++) {
data[j].axes[i]['id'] = data[j].name;
if (data[j].axes[i]['value'] > maxValue) {
maxValue = data[j].axes[i]['value'];
}
}
}
maxValue = Math.max(cfg.maxValue, maxValue);

const allAxis = data[0].axes.map((i, j) => i.axis), //Names of each axis
total = allAxis.length, //The number of different axes
radius = Math.min(cfg.w / 2, cfg.h / 2), //Radius of the outermost circle
Format = d3.format(cfg.format), //Formatting
angleSlice = (Math.PI * 2) / total; //The width in radians of each "slice"

//Scale for the radius
const rScale = d3
.scaleLinear()
.range([0, radius])
.domain([0, maxValue]);

/////////////////////////////////////////////////////////
//////////// Create the container SVG and g /////////////
/////////////////////////////////////////////////////////

//Remove whatever chart with the same id/class was present before
parent.select("svg").remove();

//Initiate the radar chart SVG
let svg = parent
.append("svg")
.attr("width", cfg.w + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom)
.attr("class", "radar");

//Append a g element
let g = svg
.append("g")
.attr(
"transform",
"translate(" +
(cfg.w / 2 + cfg.margin.left) +
"," +
(cfg.h / 2 + cfg.margin.top) +
")"
);

/////////////////////////////////////////////////////////
////////// Glow filter for some extra pizzazz ///////////
/////////////////////////////////////////////////////////

//Filter for the outside glow
let filter = g
.append('defs')
.append('filter')
.attr('id', 'glow'),
feGaussianBlur = filter
.append('feGaussianBlur')
.attr('stdDeviation', '2.5')
.attr('result', 'coloredBlur'),
feMerge = filter.append('feMerge'),
feMergeNode_1 = feMerge.append('feMergeNode').attr('in', 'coloredBlur'),
feMergeNode_2 = feMerge.append('feMergeNode').attr('in', 'SourceGraphic');

/////////////////////////////////////////////////////////
/////////////// Draw the Circular grid //////////////////
/////////////////////////////////////////////////////////

//Wrapper for the grid & axes
let axisGrid = g.append("g").attr("class", "axisWrapper");

//Draw the background circles
axisGrid
.selectAll(".levels")
.data(d3.range(1, cfg.levels + 1).reverse())
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", d => (radius / cfg.levels) * d)
.style("fill", "#CDCDCD")
.style("stroke", "#CDCDCD")
.style("fill-opacity", cfg.opacityCircles)
.style("filter", "url(#glow)");

//Text indicating at what % each level is
axisGrid
.selectAll(".axisLabel")
.data(d3.range(1, cfg.levels + 1).reverse())
.enter()
.append("text")
.attr("class", "axisLabel")
.attr("x", 4)
.attr("y", d => (-d * radius) / cfg.levels)
.attr("dy", "0.4em")
.style("font-size", "10px")
.attr("fill", "#737373")
.text(d => Format((maxValue * d) / cfg.levels) + cfg.unit);

/////////////////////////////////////////////////////////
//////////////////// Draw the axes //////////////////////
/////////////////////////////////////////////////////////

//Create the straight lines radiating outward from the center
var axis = axisGrid
.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis");
//Append the lines
axis
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr(
"x2",
(d, i) => rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI / 2)
)
.attr(
"y2",
(d, i) => rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI / 2)
)
.attr("class", "line")
.style("stroke", "white")
.style("stroke-width", "2px");

//Append the labels at each axis
axis
.append("text")
.attr("class", "legend")
.style("font-size", "11px")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr(
"x",
(d, i) =>
rScale(maxValue * cfg.labelFactor) *
Math.cos(angleSlice * i - Math.PI / 2)
)
.attr(
"y",
(d, i) =>
rScale(maxValue * cfg.labelFactor) *
Math.sin(angleSlice * i - Math.PI / 2)
)
.text(d => d)
.call(wrap, cfg.wrapWidth);

/////////////////////////////////////////////////////////
///////////// Draw the radar chart blobs ////////////////
/////////////////////////////////////////////////////////

//The radial line function
const radarLine = d3
.radialLine()
.curve(d3.curveLinearClosed)
.radius(d => rScale(d.value))
.angle((d, i) => i * angleSlice);

if (cfg.roundStrokes) {
radarLine.curve(d3.curveCardinalClosed);
}

//Create a wrapper for the blobs
const blobWrapper = g
.selectAll(".radarWrapper")
.data(data)
.enter()
.append("g")
.attr("class", "radarWrapper");

//Append the backgrounds
blobWrapper
.append("path")
.attr("class", "radarArea")
.attr("d", d => radarLine(d.axes))
.style("fill", (d, i) => cfg.color(i))
.style("fill-opacity", cfg.opacityArea)
.on('mouseover', function(d, i) {
//Dim all blobs
parent
.selectAll(".radarArea")
.transition()
.duration(200)
.style("fill-opacity", 0.1);
//Bring back the hovered over blob
d3.select(this)
.transition()
.duration(200)
.style("fill-opacity", 0.7);
})
.on('mouseout', () => {
//Bring back all blobs
parent
.selectAll(".radarArea")
.transition()
.duration(200)
.style("fill-opacity", cfg.opacityArea);
});

//Create the outlines
blobWrapper
.append("path")
.attr("class", "radarStroke")
.attr("d", function(d, i) {
return radarLine(d.axes);
})
.style("stroke-width", cfg.strokeWidth + "px")
.style("stroke", (d, i) => cfg.color(i))
.style("fill", "none")
.style("filter", "url(#glow)");

//Append the circles
blobWrapper
.selectAll(".radarCircle")
.data(d => d.axes)
.enter()
.append("circle")
.attr("class", "radarCircle")
.attr("r", cfg.dotRadius)
.attr(
"cx",
(d, i) => rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2)
)
.attr(
"cy",
(d, i) => rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2)
)
.style("fill", d => cfg.color(d.id))
.style("fill-opacity", 0.8);

/////////////////////////////////////////////////////////
//////// Append invisible circles for tooltip ///////////
/////////////////////////////////////////////////////////

//Wrapper for the invisible circles on top
const blobCircleWrapper = g
.selectAll(".radarCircleWrapper")
.data(data)
.enter()
.append("g")
.attr("class", "radarCircleWrapper");

//Append a set of invisible circles on top for the mouseover pop-up
blobCircleWrapper
.selectAll(".radarInvisibleCircle")
.data(d => d.axes)
.enter()
.append("circle")
.attr("class", "radarInvisibleCircle")
.attr("r", cfg.dotRadius * 1.5)
.attr(
"cx",
(d, i) => rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2)
)
.attr(
"cy",
(d, i) => rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2)
)
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function(d, i) {
tooltip
.attr('x', this.cx.baseVal.value - 10)
.attr('y', this.cy.baseVal.value - 10)
.transition()
.style('display', 'block')
.text(String(i.value) + cfg.unit);
})
.on("mouseout", function() {
tooltip
.transition()
.style('display', 'none')
.text('');
});

const tooltip = g
.append("text")
.attr("class", "tooltip")
.attr('x', 0)
.attr('y', 0)
.style("font-size", "12px")
.style('display', 'none')
.attr("text-anchor", "middle")
.attr("dy", "0.35em");

if (cfg.legend !== false && typeof cfg.legend === "object") {
let legendZone = svg.append('g');
let names = data.map(el => el.name);
if (cfg.legend.title) {
let title = legendZone
.append("text")
.attr("class", "title")
.attr(
'transform',
`translate(${cfg.legend.translateX},${cfg.legend.translateY})`
)
.attr("x", cfg.w - 70)
.attr("y", 10)
.attr("font-size", "12px")
.attr("fill", "#404040")
.text(cfg.legend.title);
}
let legend = legendZone
.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 200)
.attr(
'transform',
`translate(${cfg.legend.translateX},${cfg.legend.translateY + 20})`
);
// Create rectangles markers
legend
.selectAll('rect')
.data(names)
.enter()
.append("rect")
.attr("x", cfg.w - 65)
.attr("y", (d, i) => i * 20)
.attr("width", 10)
.attr("height", 10)
.style("fill", (d, i) => cfg.color(i));
// Create labels
legend
.selectAll('text')
.data(names)
.enter()
.append("text")
.attr("x", cfg.w - 52)
.attr("y", (d, i) => i * 20 + 9)
.attr("font-size", "11px")
.attr("fill", "#737373")
.text(d => d);
}
return svg;
}
Insert cell
margin = ({top: 50, right: 50, bottom: 50, left: 50})
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more