JSFiddle - React, Tailwind, and code Playground

by stitot

HTML

<script src="https://code.highcharts.com/dashboards/datagrid.js"></script>

<div style="margin: 1rem 0">
      <label>
        Layout:
        <select id="layoutSelect">
          <option value="pills" selected>Pills per product</option>
          <option value="grouped">Grouped headers: editions under product</option>
        </select>
      </label>
    </div>

    <div style="margin: 1rem 0" id="fullEditionToggle">
      <label>
        <input type="checkbox" id="showAllEditions" />
        List all editions when feature is available in every edition
      </label>
    </div>

    <div id="container"></div>

CSS

@import url('https://code.highcharts.com/dashboards/css/datagrid.css');

body {
  font-family:
    ui-sans-serif,
    -apple-system,
    system-ui,
    Segoe UI,
    Helvetica,
    Apple Color Emoji,
    Arial,
    sans-serif,
    Segoe UI Emoji,
    Segoe UI Symbol;
  background: #fff;
}

h2 {
  margin: 30px 40% 5px 0;
  text-transform: uppercase;
  font-weight: 500;
  font-size: 20px;
  padding: 0 15px;
}

p {
  margin: 0 40% 15px 0;
  font-size: 15px;
  padding: 0 15px;
  line-height: 1.3em;
}

.highcharts-datagrid-table tbody tr td {
  text-align: center;
  &.N\/A {
    background: #faeeeb !important;
    span {
      background: #c68470;
    }
  }

  &.partial {
    background: #fcf8e6 !important;
    span {
      background: #d6bf67;
    }
  }

  &.full {
    background: #f2f7e9 !important;
    span {
      background: #94b373;
    }
  }

  &.strong {
    font-weight: 500;
  }

  &.left {
    text-align: left;
  }

  a {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  a:hover {
    background: #00000007;
  }

  div.editions span {
    display: inline-block;
    padding: 3px 4px;
    color: #ffffff;
    font-size: 0.7em;
    text-transform: uppercase;
    line-height: 1em;
    margin: 2px;
    border-radius: 5px;
  }

  small {
    font-size: 14px;
  }

  &.has-comment {
    position: relative;

    &::after {
      content: '';
      position: absolute;
      top: 0;
      right: 0;
      border-top: 10px solid #999;
      border-left: 10px solid transparent;
    }
  }
}

.comment-tooltip {
  position: absolute;
  background: #333;
  color: #fff;
  padding: 4px 8px;
  border-radius: 4px;
  white-space: pre-wrap;
  z-index: 10;
  pointer-events: none;
  font-size: 14px;
  display: none;
}

.theme-features {
  --hcg-font-size:...

JavaScript

// ================== Feature Matrix → Highcharts Grid (Pills vs Grouped) ==================
// - Fetch ONCE, cache payload
// - Render either layout from the SAME dataset (no refetch)
// - If a product has NO editions → it shows in BOTH layouts:
//     • Pills: as a normal product column
//     • Grouped: as a simple (non-grouped) column (i.e., not a grouped header)
// ==========================================================================================

// ---------- Utilities ----------
const escapeHtml = (s) =>
  String(s ?? '').replace(
    /[&<>"']/g,
    (ch) => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'})[ch],
  )

const colIdFor = (productName, editionName) => `${productName} | ${editionName}`

// ---------- Core normalization ----------
function coreFromPayload(payload) {
  const data = payload && payload.result ? payload.result : payload
  const products = Array.isArray(data?.products) ? data.products : []
  const categories = Array.isArray(data?.categories) ? data.categories : []

  // Keep stable product order
  const productOrder = products.map((p) => ({
    id: p._id,
    name: p.name,
    editions: Array.isArray(p.editions) ? p.editions : [],
  }))

  return {categories, productOrder}
}

function indexSupport(supportedBy) {
  // Map productId → {editions?: string[], comment: string, supported: boolean}
  // (No coverage here; we compute it later using product's total editions)
  return new Map(
    (Array.isArray(supportedBy) ? supportedBy : []).map((s) => {
      const entry = {
        comment: typeof s?.comment === 'string' ? s.comment : '',
        url: typeof s?.url === 'string' ? s.url : '',
        supported: !!s?.supported,
      }

      if (Array.isArray(s?.editions) && s.editions.length > 0) {
        entry.editions = s.editions
      }

      return [s?._id, entry]
    }),
  )
}

// ---------- Unified builder (same output shape for BOTH layouts)...