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) => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''})[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)...