PageSourceSearch

https://www.hotelmanagement-network.com/wp-content/themes/goodlife…/assets/js/modules/GDMRemoteStatusChart.js

js hotelmanagement-network.com collected 2026-09-24 18:37:04 UTC 6,417 bytes, 200 lines download raw bytes

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, "&lt;")
45    .replace(/>/g, "&gt;")
46    .replace(/\"/g, "&quot;")
47    .replace(/'/g, "&#39;");
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.