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

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