Updated on Jul 6th, 20212 min readd3eCommerce

D3 Visualization | Bubble Chart - LADC Sample Sales

About This Visualization

This chart summarized sample sales by brand and manufacturer for my current employer. Samples of fabric and wallpaper can be purchased at a relatively inexpensive price before committing to the larger financial commitment of purchasing yardage or rolls.

The chart has two modes - brand and manufacturer. Our website sells over 70 distinct brand names, but many of these brands can be grouped my manufacturer. Generally, business relationships are on the manufacturer level, while customers are more concerned with purchasing products based on the brand level.

In brand mode, each bubble represents a brand. The radius, and therefore the size, of the bubble is determined by the number of samples sold. In manufacturer mode the same rules apply, with each brand being folded into its manufacturer.

The chart can also be filtered by manufacturer to drill down and examine specific brands.

The result is an overview of performance in sample sales.

How This Visualization Was Created

I used the online platform Observable to help create this visualization. Observable makes it easier to quickly share ideas and code, especially when it comes to interactive data visualizations. It is also relatively easy to take parts of an Observable project and embed them into a website, like I have done here.

The primary library used to create this bubble chart visualization is D3. Data Driven Documents is a JavaScript library that supplies many of the tools needed to organize data and eventually render interactive SVG elements that represent that underlying data.

I used the WooCommerce REST API to crawl through orders and pull out the relevant line items for samples. I built a simple script with Node that consumed the orders for this year and output a CSV file with the order number, product SKU, and product name for each sample line item.

This is the exact script I used to gather raw data from the WooCommerce REST API (is built into every WooCommerce site). Samples are something of a custom product type, so I had to take that into consideration. Essentially, the sample is a singular product, with specific meta data assigned dynamically to describe it. This Node script was created separate and apart from the Observable notebook.

const WooCommerceRestApi = require('@woocommerce/woocommerce-rest-api').default
const { createWriteStream } = require('fs')

// Establish WooCommerce REST API connection
const WooCommerce = new WooCommerceRestApi({
  url: 'https://example.com',
  consumerKey: process.env.WOOCOMMERCE_KEY,
  consumerSecret: process.env.WOOCOMMERCE_SECRET,
  version: 'wc/v3',

// Create write stream to CSV file
const writer = createWriteStream('data.csv')

async function main() {
  var page = 1

  while (page <= 124) {
    const res1 = await WooCommerce.get(`orders`, {
      per_page: 100,

    for (let order of res1.data) {
      for (let item of order.line_items) {
        if (
          item.name === 'Sample' &&
          item.meta_data[0].hasOwnProperty('key') &&
          item.meta_data[0]['key'] === 'Name' &&
          item.meta_data[1].hasOwnProperty('key') &&
          item.meta_data[1]['key'] === 'SKU'
        ) {
          let name = item.meta_data.find((el) => el.key === 'Name')['value']
          let sku = item.meta_data.find((el) => el.key === 'SKU')['value']


    page += 1


The resulting CSV file can be directly attached to an Observable notebook, or can be placed into a public Gist and imported via URL.

Within Observable I am able to dynamically change the data powering the visualization by using the groupBy variable. This groupBy is attached to the radio button inputs above.

sampleData = {
  const data = []
  const raw = await d3.csv('https://gist.githubusercontent.com/benjaminadk/cafbad398de0a1f8c49a5446f417beb1/raw/7c61ae88021f37a972961a4071dc62b309f7c018/ladc-sample-sales.csv')

  for(let r of Array.from(raw)) {
    for(let b of brands) {
      let mod = groupBy === 'brand' ? b.name : b.manf
      if(r.name.startsWith(b.short)) {
        if(data.find(el => el.brand === mod)) {
          data.find(el => el.brand === mod)['samples'] += 1
        } else {
          let id = groupBy === 'brand' ? b.abbr : manufacturers.find(el => el.name === b.manf)['id']
          data.push({id, brand: mod, manf: b.manf, samples: 1})

  return data
Benjamin Brooke Avatar
Benjamin Brooke

Hi, I'm Ben. I work as a full stack developer for an eCommerce company. My goal is to share knowledge through my blog and courses. In my free time I enjoy cycling and rock climbing.