Public
Edited
Dec 22, 2023
Insert cell
Insert cell
chart = {
// set dimensions
const dimensions = {
width: 675,
height: 500
};

// canvas
const svg = d3
.create("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height);

const canvas = html`
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Poppins:wght@700&display=swap');

.chart-container {
padding-top: 0.25em;
font-family: "Inter", sans-serif;
}

@media screen and (max-width: 800px) {
/* scrollable on mobile */
.chart-mobile-container {
display: flex;
flex-wrap: nowrap;
overflow-x: scroll;
padding: 0;
}
/* scroll horizontally */
.chart-mobile-container::-webkit-scrollbar {
display: none;
}
}
</style>
<div class="chart-mobile-container">
<div class="chart-container">
<div class="chart">${svg.node()}</div>
</div>
</div>
`;

const bubblesSVG = svg
.append("g")
.attr(
"transform",
`translate(${dimensions.width / 3},${dimensions.height / 2})`
);

// color palette
const colorScale = d3
.scaleOrdinal()
.domain([
"Community media",
"Traditional media",
"Unspecified traditional media",
"Unspecified digital media",
"Unspecified media placements/buys",
"Social media and tech",
"Ad serving/targeting companies",
"Sports and live events marketing"
])
.range([
"#F7E987",
"#4489E3",
"#57C9FA",
"#b298dc",
"#bdbdbd",
"#FF7D5E",
"#FFA551", //"#FFB26B",
"#A8DF8E"
]);

// establish radius of bubbles
const radiusScale = d3
.scaleSqrt()
.domain([0, d3.max(data, (d) => d.sum)])
.range([1, 125]);

// bubble force
const simulation = d3
.forceSimulation()
.force("x", d3.forceX().strength(0.1))
.force("y", d3.forceY().strength(0.1))
.force(
"collide",
d3.forceCollide((d) => radiusScale(d.sum) + 2) // space btwn circles
);

// Draw bubbles
const bubbles = bubblesSVG
.selectAll("circle")
.data(data)
.join("circle")
.attr("r", (d) => radiusScale(d.sum))
.attr("fill", (d) => colorScale(d.chart_category))
.attr("stroke", (d) => newShade(colorScale(d.chart_category), -50))
.attr("stroke-width", "1.1");

// Text: Pct labels
const pctLabels = bubblesSVG
.selectAll("text")
.data(data)
.join("text")
.attr("class", "labels")
.attr("text-anchor", "middle")
.text((d) =>
d.chart_category === "Community media" ||
d.chart_category === "Traditional media"
? null
: d.chart_category === "Ad serving/targeting companies"
? `${d3.format(",.1~f")(d.pct)}%`
: `${d3.format(",.1~f")(d.pct)}%`
)
.attr("dy", (d) =>
d.chart_category === "Ad serving/targeting companies"
? "0.3em"
: "-0.25em"
)
.style("font-size", (d) =>
d.chart_category === "Ad serving/targeting companies"
? "1.2rem"
: "1.5rem"
)
.style("font-weight", "500")
.style("fill", (d) =>
d.chart_category === "Community media" ? "#ffffff" : "#333333"
);

// Text: Category labels
const catLabels = pctLabels
.append("tspan")
.attr("x", 0)
.attr("dy", "0.75em")
.text((d) =>
d.chart_category.match(
/Community media|Traditional media|Unspecified traditional media|Unspecified digital media|Unspecified media placements\/buys|Social media and tech|Ad serving\/targeting companies|Sports and live events marketing/
)
? d.chart_category
.replace("Community media", "")
.replace("Traditional media", "")
.replace("Unspecified traditional media", "Unspecified traditional")
.replace("Unspecified digital media", "Unspecified digital")
.replace("Unspecified media placements/buys", "Unspecified buys")
.replace("Social media and tech", "Social")
.replace("Ad serving/targeting companies", "")
.replace("Sports and live events marketing", "Sports + events")
: d.chart_category
)
.style("font-size", (d) =>
d.chart_category === "Community media" ||
d.chart_category === "Sports and live events marketing"
? "1.05rem"
: "1.05rem"
)
.style("font-weight", "500")
.style(
"fill",
(d) => (d.chart_category === "Community media" ? "#eeeeee" : "#333333") // "#4F4557"
);

// Text: $ amts
const amtLabels = catLabels
.append("tspan")
.attr("x", 0)
.attr("dy", "2.1em")
.html((d) =>
d.chart_category === "Community media" ||
d.chart_category === "Traditional media" ||
d.chart_category === "Ad serving/targeting companies"
? null
: `${d3.format("$,.3s")(d.sum).toLowerCase()}`
)
.style("font-size", (d) =>
d.chart_category === "Community media" //||
? // d.chart_category === "Social media and tech" ||
// d.chart_category === "Sports and live events marketing"
"1rem"
: "1rem"
)
.style("font-weight", "600")
.style(
"fill",
(d) =>
d.chart_category === "Community media"
? "#eeeeee"
: newShade(colorScale(d.chart_category), -75) //"#665970"
);

///////////////
// ANNOTATIONS
///////////////

// Text: Unspecified + Hispanic
const unspefX = 400;
const unspefY = 250;
const unspefDy = "1.25em";
svg
.append("text")
.style("font-weight", "400")
.style("font-size", "0.9em")
//
.html(`● 1.3% Ad serving companies ($61k)`)
.style("fill", "#fe8c39") // "#FFB26B") //"#fe8c39")
.attr("x", `${unspefX}`)
.attr("y", `${unspefY}`)
.style("font-weight", "500")
//
.append("tspan")
.text("● 0.1% Community media ($3.5k)")
.style("fill", "#eeb304")
.attr("x", `${unspefX}`)
.attr("dy", `${unspefDy}`)
.style("font-weight", "500")
//
.append("tspan")
.text("● 0.1% Traditional media ($2.5k)")
.style("fill", "#4489E3")
.attr("x", `${unspefX}`)
.attr("dy", `${unspefDy}`)
.style("font-weight", "500");

///////////////
// ARROW
///////////////
// const points = [
// { x: 350, y: 150 },
// { x: 390, y: 125 }
// ];

// const lineGen = d3
// .line()
// .x((d) => d.x)
// .y((d) => d.y)
// .curve(d3.curveNatural);

// // arrowhead
// svg
// .append("svg:defs")
// .append("svg:marker")
// .attr("id", "triangle")
// .attr("refX", 6)
// .attr("refY", 6)
// .attr("markerWidth", 15)
// .attr("markerHeight", 15)
// .attr("orient", "auto")
// .append("path")
// .attr("d", "M 0 0 8 6 0 12 4 6")
// .style("fill", "black");

// // line for arrow
// svg
// .append("path")
// .attr("d", lineGen(points))
// .attr("marker-end", "url(#arrow)")
// .attr("fill", "none")
// .attr("stroke", "#777777")
// .attr("marker-end", "url(#triangle)");
// //.attr("transform", `translate(50, -50)`);

simulation.nodes(data).on("tick", ticked);

function ticked() {
bubbles.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
pctLabels.attr("x", (d) => d.x).attr("y", (d) => d.y);
catLabels.attr("x", (d) => d.x).attr("y", (d) => d.y);
amtLabels.attr("x", (d) => d.x).attr("y", (d) => d.y);
}

return canvas;
}
Insert cell
Insert cell
Insert cell
Inputs.table(data_raw)
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