1const SEGMENTS = [ 2 { key: "onsite", label: "Onsite", color: "#72c18f" }, 3 { key: "remote", label: "Remote", color: "#f5cf4e" }, 4 { key: "notSpecific", label: "Not Specific", color: "#62b4ea" }, 5]; 6 7const CHART_CONFIG = { 8 half: { 9 centerX: 180, 10 centerY: 180, 11 endAngle: 90, 12 gapPx: 4, 13 radius: 124, 14 startAngle: -90, 15 totalClassName: "donut-total donut-total--half", 16 viewBox: "0 0 360 220", 17 }, 18 full: { 19 centerX: 180, 20 centerY: 180, 21 endAngle: 270, 22 gapPx: 2, 23 radius: 124, 24 startAngle: -90, 25 totalClassName: "donut-total donut-total--full", 26 viewBox: "0 0 360 360", 27 }, 28}; 29 30const getValue = (root, attributeName) => { 31 const raw = Number.parseInt(root.getAttribute(attributeName) || "0", 10); 32 return Number.isFinite(raw) ? Math.max(0, raw) : 0; 33}; 34 35const normaliseItem = (item, index) => ({ 36 color: item?.color || SEGMENTS[index % SEGMENTS.length]?.color || "#62b4ea", 37 label: item?.label || `Item ${index + 1}`, 38 value: Number.isFinite(Number(item?.value)) ? Math.max(0, Number(item.value)) : 0, 39}); 40 41const escapeHtml = (value) => 42 String(value) 43 .replace(/&/g, "&") 44 .replace(/</g, "<") 45 .replace(/>/g, ">") 46 .replace(/\"/g, """) 47 .replace(/'/g, "'"); 48 49const parseJsonConfig = (root) => { 50 const dataNode = root.querySelector(".gdm-chart__data"); 51 if (!dataNode) return null; 52 53 try { 54 return JSON.parse(dataNode.textContent || "{}"); 55 } catch (error) { 56 console.error("Invalid donut chart config", error); 57 return null; 58 } 59}; 60 61const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => { 62 const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180; 63 64 return { 65 x: centerX + radius * Math.cos(angleInRadians), 66 y: centerY + radius * Math.sin(angleInRadians), 67 }; 68}; 69 70const describeArc = (centerX, centerY, radius, startAngle, endAngle) => { 71 const start = polarToCartesian(centerX, centerY, radius, endAngle); 72 const end = polarToCartesian(centerX, centerY, radius, startAngle); 73 const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; 74 75 return [ 76 "M", 77 start.x, 78 start.y, 79 "A", 80 radius, 81 radius, 82 0, 83 largeArcFlag, 84 0, 85 end.x, 86 end.y, 87 ].join(" "); 88}; 89 90const buildArcMarkup = (items, variantName, totalLabel, total) => { 91 const variant = CHART_CONFIG[variantName] || CHART_CONFIG.half; 92 const totalAngle = variant.endAngle - variant.startAngle; 93 const gapAngle = (variant.gapPx / variant.radius) * (180 / Math.PI); 94 const chartTotal = items.reduce((sum, item) => sum + item.value, 0) || 1; 95 const renderAllBoundaries = variantName === "full"; 96 97 let currentAngle = variant.startAngle; 98 const paths = items 99 .filter((item) => item.value > 0) 100 .map((item, index, visibleItems) => { 101 const segmentAngle = (item.value / chartTotal) * totalAngle; 102 const hasLeadingGap = renderAllBoundaries || index > 0; 103 const hasTrailingGap = renderAllBoundaries || index < visibleItems.length - 1; 104 const safeGapAngle = Math.min(gapAngle, Math.max(segmentAngle - 0.1, 0)); 105 const halfGapAngle = safeGapAngle / 2; 106 const segmentStart = currentAngle + (hasLeadingGap ? halfGapAngle : 0); 107 const segmentEnd = currentAngle + segmentAngle - (hasTrailingGap ? halfGapAngle : 0); 108 109 currentAngle += segmentAngle; 110 111 if (segmentEnd <= segmentStart) { 112 return ""; 113 } 114 115 return `<path d="${describeArc(variant.centerX, variant.centerY, variant.radius, segmentStart, segmentEnd)}" stroke="${escapeHtml(item.color)}" />`; 116 }) 117 .join(""); 118 119 const rows = items 120 .map( 121 (item) => ` 122 <div class="donut-legend-row"> 123 <span class="donut-label-wrap"> 124 <span class="donut-dot" style="background:${escapeHtml(item.color)}"></span> 125 <span class="donut-label">${escapeHtml(item.label)}</span> 126 </span> 127 <span class="donut-value">${item.value}</span> 128 </div> 129 `, 130 ) 131 .join(""); 132 133 return ` 134 <div class="donut-figure donut-figure--${variantName}"> 135 <svg class="donut-svg" viewBox="${variant.viewBox}" aria-hidden="true" focusable="false"> 136 <g class="donut-arcs">${paths}</g> 137 </svg> 138 <div class="${variant.totalClassName}">
139 <span class="donut-total-label">${escapeHtml(totalLabel)}</span> 140 <strong class="donut-total-value">${total}</strong> 141 </div> 142 </div> 143 <div class="donut-legend">${rows}</div> 144 `; 145}; 146 147const getChartConfig = (root) => { 148 const config = parseJsonConfig(root); 149 150 if (config?.items?.length) { 151 const items = config.items.map(normaliseItem); 152 const total = Number.isFinite(Number(config.total)) 153 ? Math.max(0, Number(config.total)) 154 : items.reduce((sum, item) => sum + item.value, 0); 155 156 return { 157 items, 158 legendLabel: config.legendLabel || `${config.title || "Chart"} breakdown`, 159 title: config.title || root.getAttribute("data-chart-title") || "Chart", 160 total, 161 totalLabel: config.totalLabel || "Total Jobs:", 162 variant: config.variant === "full" ? "full" : "half", 163 }; 164 } 165 166 const items = [ 167 { ...SEGMENTS[0], value: getValue(root, "data-onsite") }, 168 { ...SEGMENTS[1], value: getValue(root, "data-remote") }, 169 { ...SEGMENTS[2], value: getValue(root, "data-not-specific") }, 170 ].map(normaliseItem); 171 172 return { 173 items, 174 legendLabel: root.getAttribute("data-chart-legend-label") || "Jobs by remote status breakdown", 175 title: root.getAttribute("data-chart-title") || "Jobs by remote status", 176 total: items.reduce((sum, item) => sum + item.value, 0), 177 totalLabel: root.getAttribute("data-total-label") || "Total Jobs:", 178 variant: root.getAttribute("data-chart-variant") === "full" ? "full" : "half", 179 }; 180}; 181 182export default function GDMRemoteStatusChart({ rootSelector = "[data-gdm-remote-status-chart]" } = {}) { 183 const roots = document.querySelectorAll(rootSelector); 184 if (!roots.length) return; 185 186 roots.forEach((root) => { 187 const { items, legendLabel, title, total, totalLabel, variant } = getChartConfig(root); 188 189 root.setAttribute("data-chart-variant", variant); 190 191 root.innerHTML = ` 192 <header class="header center"> 193 <h3 class="title">${escapeHtml(title)}</h3> 194 </header> 195 <div class="donut-chart" aria-label="${escapeHtml(legendLabel)}"> 196 ${buildArcMarkup(items, variant, totalLabel, total)} 197 </div> 198 `; 199 }); 200}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.