From 837a18aeb8a69a21f4887898facc063c77ab35f9 Mon Sep 17 00:00:00 2001 From: Andrii Hazin <13032566+ndrhzn@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:28:27 +0200 Subject: [PATCH] feat: simple web app to demonstrate dependencies betwetn ocds fields and procurement indicators --- ocds_fields_indicators/README.md | 1 + ocds_fields_indicators/index.html | 102 +++++ ocds_fields_indicators/indicators.csv | 308 +++++++++++++ ocds_fields_indicators/main.js | 560 ++++++++++++++++++++++++ ocds_fields_indicators/style.css | 597 ++++++++++++++++++++++++++ 5 files changed, 1568 insertions(+) create mode 100644 ocds_fields_indicators/README.md create mode 100644 ocds_fields_indicators/index.html create mode 100644 ocds_fields_indicators/indicators.csv create mode 100644 ocds_fields_indicators/main.js create mode 100644 ocds_fields_indicators/style.css diff --git a/ocds_fields_indicators/README.md b/ocds_fields_indicators/README.md new file mode 100644 index 0000000..abe1602 --- /dev/null +++ b/ocds_fields_indicators/README.md @@ -0,0 +1 @@ +# ocp-ocds-fields-indicators \ No newline at end of file diff --git a/ocds_fields_indicators/index.html b/ocds_fields_indicators/index.html new file mode 100644 index 0000000..9eddeb4 --- /dev/null +++ b/ocds_fields_indicators/index.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Field-Indicator Dependency Demo + + + + + +
+ +
+
+ +
+
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/ocds_fields_indicators/indicators.csv b/ocds_fields_indicators/indicators.csv new file mode 100644 index 0000000..231b3e2 --- /dev/null +++ b/ocds_fields_indicators/indicators.csv @@ -0,0 +1,308 @@ +indicator,usecase,fields +Total number of procedures,Market Opportunity,ocid +Total number of procuring entities,Market Opportunity,ocid +Total number of procuring entities,Market Opportunity,tender/procuringEntity/name +Total number of procuring entities,Market Opportunity,buyer/name +Total number of procuring entities,Market Opportunity,parties/identifier/id +Total number of procuring entities,Market Opportunity,parties/identifier/name +Total number of procuring entities,Market Opportunity,parties/roles +Total number of unique bidders,Market Opportunity,ocid +Total number of unique bidders,Market Opportunity,tender/tenderers/id +Total number of unique bidders,Market Opportunity,bids/details/tenderers/id +Total number of awarded suppliers,Market Opportunity,awards/id +Total number of awarded suppliers,Market Opportunity,awards/suppliers/id +Total number of awarded suppliers,Market Opportunity,awards/suppliers/name +Total number of awarded suppliers,Market Opportunity,awards/status +Total number of procedures by year or month,Market Opportunity,ocid +Total number of procedures by year or month,Market Opportunity,date +Total value awarded,Market Opportunity,ocid +Total value awarded,Market Opportunity,awards/status +Total value awarded,Market Opportunity,awards/value/amount +Total value awarded,Market Opportunity,awards/value/currency +Share of procedures by status,Market Opportunity,ocid +Share of procedures by status,Market Opportunity,tender/status +Number of procedures by item type,Market Opportunity,ocid +Number of procedures by item type,Market Opportunity,tender/items/classification/id +Number of procedures by item type,Market Opportunity,tender/items/classification/scheme +Proportion of procedures by procurement category,Market Opportunity,tender/mainProcurementCategory +Percent of tenders by procedure type,Market Opportunity,ocid +Percent of tenders by procedure type,Market Opportunity,tender/procurementMethod +Percent of tenders awarded by means of competitive procedures,Market Opportunity,ocid +Percent of tenders awarded by means of competitive procedures,Market Opportunity,tender/procurementMethod +Percent of tenders awarded by means of competitive procedures,Market Opportunity,awards/status +Percent of contracts awarded under each procedure type,Market Opportunity,ocid +Percent of contracts awarded under each procedure type,Market Opportunity,tender/procurementMethod +Percent of contracts awarded under each procedure type,Market Opportunity,contracts/id +Percent of contracts awarded under each procedure type,Market Opportunity,contracts/status +Total contracted value awarded under each procedure type,Market Opportunity,ocid +Total contracted value awarded under each procedure type,Market Opportunity,tender/procurementMethod +Total contracted value awarded under each procedure type,Market Opportunity,contracts/id +Total contracted value awarded under each procedure type,Market Opportunity,contracts/status +Total contracted value awarded under each procedure type,Market Opportunity,contracts/value/amount +Total contracted value awarded under each procedure type,Market Opportunity,contracts/value/currency +Total awarded value of tenders awarded by means of competitive procedures,Market Opportunity,ocid +Total awarded value of tenders awarded by means of competitive procedures,Market Opportunity,tender/procurementMethod +Total awarded value of tenders awarded by means of competitive procedures,Market Opportunity,awards/status +Total awarded value of tenders awarded by means of competitive procedures,Market Opportunity,awards/value/amount +Total awarded value of tenders awarded by means of competitive procedures,Market Opportunity,awards/value/currency +Proportion of single bid tenders,Market Opportunity,ocid +Proportion of single bid tenders,Market Opportunity,tender/procurementMethod +Proportion of single bid tenders,Market Opportunity,tender/numberOfTenderers +Proportion of single bid tenders,Market Opportunity,tender/tenderers/id +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,ocid +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,tender/procurementMethod +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,awards/status +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,awards/value/amount +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,awards/value/currency +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,tender/numberOfTenderers +Proportion of value awarded in single bid tenders vs competitive tenders,Market Opportunity,tender/tenderers/id +Mean number of bidders per tender,Market Opportunity,ocid +Mean number of bidders per tender,Market Opportunity,tender/procurementMethod +Mean number of bidders per tender,Market Opportunity,tender/numberOfTenderers +Mean number of bidders per tender,Market Opportunity,tender/tenderers/id +Median number of bidders per tender,Market Opportunity,ocid +Median number of bidders per tender,Market Opportunity,tender/procurementMethod +Median number of bidders per tender,Market Opportunity,tender/numberOfTenderers +Median number of bidders per tender,Market Opportunity,tender/tenderers/id +Mean number of bidders by item type,Market Opportunity,ocid +Mean number of bidders by item type,Market Opportunity,tender/procurementMethod +Mean number of bidders by item type,Market Opportunity,tender/items/classification/id +Mean number of bidders by item type,Market Opportunity,tender/items/classification/scheme +Mean number of bidders by item type,Market Opportunity,tender/numberOfTenderers +Mean number of bidders by item type,Market Opportunity,tender/tenderers/id +Number of suppliers by item type,Market Opportunity,awards/id +Number of suppliers by item type,Market Opportunity,awards/suppliers/id +Number of suppliers by item type,Market Opportunity,awards/suppliers/name +Number of suppliers by item type,Market Opportunity,awards/items/classification/id +Number of suppliers by item type,Market Opportunity,awards/items/classification/scheme +Number of new bidders in a system,Market Opportunity,tender/id +Number of new bidders in a system,Market Opportunity,tender/tenderers/id +Number of new bidders in a system,Market Opportunity,tender/tenderPeriod/startDate +Percent of new bidders to all bidders,Market Opportunity,tender/id +Percent of new bidders to all bidders,Market Opportunity,tender/tenderers/id +Percent of new bidders to all bidders,Market Opportunity,tender/tenderPeriod/startDate +Percent of tenders with at least three participants deemed qualified,Market Opportunity,ocid +Percent of tenders with at least three participants deemed qualified,Market Opportunity,bids/details/tenderers/id +Percent of tenders with at least three participants deemed qualified,Market Opportunity,bids/details/id +Percent of tenders with at least three participants deemed qualified,Market Opportunity,bids/details/status +Mean percent of bids which are disqualified,Market Opportunity,tender/id +Mean percent of bids which are disqualified,Market Opportunity,bids/details/id +Mean percent of bids which are disqualified,Market Opportunity,bids/details/status +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,awards/id +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,awards/suppliers/id +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,awards/suppliers/name +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,contracts/id +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,contracts/awardID +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,contracts/value/amount +Percent of contracts awarded to top 10 suppliers with largest contracted totals,Market Opportunity,contracts/value/currency +Mean number of unique suppliers per buyer,Market Opportunity,ocid +Mean number of unique suppliers per buyer,Market Opportunity,awards/suppliers/id +Mean number of unique suppliers per buyer,Market Opportunity,awards/suppliers/name +Mean number of unique suppliers per buyer,Market Opportunity,tender/procuringEntity/name +Mean number of unique suppliers per buyer,Market Opportunity,buyer/name +Mean number of unique suppliers per buyer,Market Opportunity,parties/identifier/id +Mean number of unique suppliers per buyer,Market Opportunity,parties/roles +Number of new awarded suppliers,Market Opportunity,awards/id +Number of new awarded suppliers,Market Opportunity,awards/suppliers/id +Number of new awarded suppliers,Market Opportunity,awards/suppliers/name +Number of new awarded suppliers,Market Opportunity,awards/date +Percent of awards awarded to new suppliers,Market Opportunity,awards/id +Percent of awards awarded to new suppliers,Market Opportunity,awards/suppliers/id +Percent of awards awarded to new suppliers,Market Opportunity,awards/suppliers/name +Percent of awards awarded to new suppliers,Market Opportunity,awards/date +Total awarded value awarded to new suppliers,Market Opportunity,awards/id +Total awarded value awarded to new suppliers,Market Opportunity,awards/suppliers/id +Total awarded value awarded to new suppliers,Market Opportunity,awards/suppliers/name +Total awarded value awarded to new suppliers,Market Opportunity,awards/date +Total awarded value awarded to new suppliers,Market Opportunity,awards/value/amount +Total awarded value awarded to new suppliers,Market Opportunity,awards/value/currency +Percent of new suppliers to all suppliers,Market Opportunity,awards/id +Percent of new suppliers to all suppliers,Market Opportunity,awards/suppliers/id +Percent of new suppliers to all suppliers,Market Opportunity,awards/suppliers/name +Percent of new suppliers to all suppliers,Market Opportunity,awards/date +Percent of growth of new awarded suppliers in a system,Market Opportunity,awards/id +Percent of growth of new awarded suppliers in a system,Market Opportunity,awards/suppliers/id +Percent of growth of new awarded suppliers in a system,Market Opportunity,awards/suppliers/name +Percent of growth of new awarded suppliers in a system,Market Opportunity,awards/date +Percent of total awarded value awarded to recurring suppliers,Market Opportunity,awards/id +Percent of total awarded value awarded to recurring suppliers,Market Opportunity,awards/suppliers/id +Percent of total awarded value awarded to recurring suppliers,Market Opportunity,awards/suppliers/name +Percent of total awarded value awarded to recurring suppliers,Market Opportunity,awards/date +Percent of total awarded value awarded to recurring suppliers,Market Opportunity,awards/value/amount +Percent of total awarded value awarded to recurring suppliers,Market Opportunity,awards/value/currency +Mean number of bids necessary to win,Market Opportunity,ocid +Mean number of bids necessary to win,Market Opportunity,tender/tenderers/id +Mean number of bids necessary to win,Market Opportunity,awards/suppliers/id +Mean number of bids necessary to win,Market Opportunity,awards/suppliers/name +"Market concentration, market share of the largest company in the market",Market Opportunity,awards/suppliers/id +"Market concentration, market share of the largest company in the market",Market Opportunity,awards/suppliers/name +"Market concentration, market share of the largest company in the market",Market Opportunity,awards/value/amount +"Market concentration, market share of the largest company in the market",Market Opportunity,awards/value/currency +"Market concentration, market share of the largest company in the market",Market Opportunity,awards/items/classification/id +"Market concentration, market share of the largest company in the market",Market Opportunity,awards/items/classification/scheme +Proportion of contracts awarded by supplier by non competitive procedures,Market Opportunity,ocid +Proportion of contracts awarded by supplier by non competitive procedures,Market Opportunity,tender/procurementMethod +Proportion of contracts awarded by supplier by non competitive procedures,Market Opportunity,awards/status +Proportion of contracts awarded by supplier by non competitive procedures,Market Opportunity,awards/suppliers/id +Proportion of contracts awarded by supplier by non competitive procedures,Market Opportunity,awards/suppliers/name +Region of the supplier,Market Opportunity,parties/roles +Region of the supplier,Market Opportunity,parties/identifier/id +Region of the supplier,Market Opportunity,parties/address/region +Region of the supplier,Market Opportunity,parties/address/addressDetails/region +Number of bids submitted by supplier,Market Opportunity,awards/suppliers/id +Number of bids submitted by supplier,Market Opportunity,tender/tenderers/id +Number of bids submitted by supplier,Market Opportunity,bids/details/tenderers/id +Success rate of bidders,Market Opportunity,ocid +Success rate of bidders,Market Opportunity,tender/tenderers/id +Success rate of bidders,Market Opportunity,awards/suppliers/id +Success rate of bidders,Market Opportunity,awards/suppliers/name +Number of unique items classifications awarded by supplier,Market Opportunity,awards/id +Number of unique items classifications awarded by supplier,Market Opportunity,awards/suppliers/id +Number of unique items classifications awarded by supplier,Market Opportunity,awards/suppliers/name +Number of unique items classifications awarded by supplier,Market Opportunity,awards/items/classification/id +Number of unique items classifications awarded by supplier,Market Opportunity,awards/items/classification/scheme +Total value awarded by supplier,Market Opportunity,awards/id +Total value awarded by supplier,Market Opportunity,awards/suppliers/id +Total value awarded by supplier,Market Opportunity,awards/suppliers/name +Total value awarded by supplier,Market Opportunity,awards/status +Total value awarded by supplier,Market Opportunity,contracts/id +Total value awarded by supplier,Market Opportunity,contracts/awardID +Total value awarded by supplier,Market Opportunity,contracts/value/amount +Total value awarded by supplier,Market Opportunity,contracts/value/currency +Share of total value awarded by supplier,Market Opportunity,awards/id +Share of total value awarded by supplier,Market Opportunity,awards/suppliers/id +Share of total value awarded by supplier,Market Opportunity,awards/suppliers/name +Share of total value awarded by supplier,Market Opportunity,awards/status +Share of total value awarded by supplier,Market Opportunity,contracts/id +Share of total value awarded by supplier,Market Opportunity,contracts/awardID +Share of total value awarded by supplier,Market Opportunity,contracts/value/amount +Share of total value awarded by supplier,Market Opportunity,contracts/value/currency +Total number of contracts awarded by supplier,Market Opportunity,awards/id +Total number of contracts awarded by supplier,Market Opportunity,awards/suppliers/id +Total number of contracts awarded by supplier,Market Opportunity,awards/suppliers/name +Total number of contracts awarded by supplier,Market Opportunity,contracts/status +Total number of contracts awarded by supplier,Market Opportunity,contracts/id +Total number of contracts awarded by supplier,Market Opportunity,contracts/awardID +Number of procuring entities by supplier,Market Opportunity,ocid +Number of procuring entities by supplier,Market Opportunity,awards/suppliers/id +Number of procuring entities by supplier,Market Opportunity,awards/suppliers/name +Number of procuring entities by supplier,Market Opportunity,tender/procuringEntity/name +Number of procuring entities by supplier,Market Opportunity,buyer/name +Number of procuring entities by supplier,Market Opportunity,parties/identifier/id +Number of procuring entities by supplier,Market Opportunity,parties/roles +Share of single bid awards by supplier,Market Opportunity,ocid +Share of single bid awards by supplier,Market Opportunity,awards/suppliers/id +Share of single bid awards by supplier,Market Opportunity,awards/suppliers/name +Share of single bid awards by supplier,Market Opportunity,awards/status +Share of single bid awards by supplier,Market Opportunity,tender/procurementMethod +Share of single bid awards by supplier,Market Opportunity,tender/numberOfTenderers +Share of single bid awards by supplier,Market Opportunity,tender/tenderers/id +Percent of tenders with linked procurement plans,Public Integrity,tender/id +Percent of tenders with linked procurement plans,Public Integrity,tender/documents/documentType +Percent of contracts which publish information on debarments,Public Integrity,contracts/id +Percent of contracts which publish information on debarments,Public Integrity,contracts/implementation/documents/documentType +The percent of tenders for which the tender documentation was added after publication of the announcement,Public Integrity,tender/id +The percent of tenders for which the tender documentation was added after publication of the announcement,Public Integrity,tender/documents/documentType +The percent of tenders for which the tender documentation was added after publication of the announcement,Public Integrity,tender/documents/documentType +The percent of tenders for which the tender documentation was added after publication of the announcement,Public Integrity,tender/documents/datePublished +Mean number of contract amendments per buyer,Public Integrity,ocid +Mean number of contract amendments per buyer,Public Integrity,contracts/id +Mean number of contract amendments per buyer,Public Integrity,contracts/amendments +Mean number of contract amendments per buyer,Public Integrity,tender/procuringEntity/name +Mean number of contract amendments per buyer,Public Integrity,buyer/name +Mean number of contract amendments per buyer,Public Integrity,parties/identifier/id +Mean number of contract amendments per buyer,Public Integrity,parties/roles +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,tender/id +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,tender/tenderPeriod/endDate +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,awards/id +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,awards/date +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,awards/status +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,awards/value/amount +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,awards/suppliers/id +"Percent of tenders which have been closed for more than 30 days, but whose basic awards information is not published",Public Integrity,awards/suppliers/name +"Percent of awards which are older than 30 days, but whose contract is not published",Public Integrity,awards/id +"Percent of awards which are older than 30 days, but whose contract is not published",Public Integrity,awards/date +"Percent of awards which are older than 30 days, but whose contract is not published",Public Integrity,contracts/awardID +"Percent of awards which are older than 30 days, but whose contract is not published",Public Integrity,contracts/status +"Percent of awards which are older than 30 days, but whose contract is not published",Public Integrity,contracts/dateSigned +"Percent of awards which are older than 30 days, but whose contract is not published",Public Integrity,contracts/documents/documentType +Percent of tenders that do not specify place of delivery,Public Integrity,ocid +Percent of tenders that do not specify place of delivery,Public Integrity,tender/items/deliveryLocation +Percent of tenders that do not specify place of delivery,Public Integrity,tender/items/deliveryAddress +Percent of tenders that do not specify date of delivery,Public Integrity,tender/milestones/id +Percent of tenders that do not specify date of delivery,Public Integrity,tender/milestones/type +Percent of tenders that do not specify date of delivery,Public Integrity,tender/milestones/description +Percent of tenders that do not specify date of delivery,Public Integrity,tender/milestones/dueDate +Percent of tenders with short titles for example fewer than 10 characters in the title,Public Integrity,tender/id +Percent of tenders with short titles for example fewer than 10 characters in the title,Public Integrity,tender/title +Percent of tenders with short descriptions for instance fewer than 30 characters in the description,Public Integrity,tender/id +Percent of tenders with short descriptions for instance fewer than 30 characters in the description,Public Integrity,tender/description +Percent of tenders that do not include detailed item codes or item descriptions,Public Integrity,tender/id +Percent of tenders that do not include detailed item codes or item descriptions,Public Integrity,tender/items/classification/id +Percent of tenders that do not include detailed item codes or item descriptions,Public Integrity,tender/items/classification/scheme +Percent of contracts that do not have amendments,Public Integrity,contracts/id +Percent of contracts that do not have amendments,Public Integrity,contracts/amendments +Percent of contracts which publish contract implementation details financial,Service Delivery,contracts/implementation/transactions/id +Percent of contracts which publish contract implementation details financial,Service Delivery,contracts/implementation/transactions/value/amount +Percent of contracts which publish contract implementation details financial,Service Delivery,contracts/implementation/transactions/value/currency +Percent of contracts which publish contract implementation details physical,Service Delivery,contracts/implementation/milestones/type +Percent of contracts which publish contract implementation details physical,Service Delivery,contracts/implementation/milestones/id +Percent of contracts which publish contract implementation details physical,Service Delivery,contracts/implementation/milestones/dueDate +Percent of contracts which publish contract implementation details physical,Service Delivery,contracts/implementation/milestones/status +Average duration of tendering period days,Internal Efficiency,ocid +Average duration of tendering period days,Internal Efficiency,tender/tenderPeriod/startDate +Average duration of tendering period days,Internal Efficiency,tender/tenderPeriod/endDate +Average duration of decision period days,Internal Efficiency,ocid +Average duration of decision period days,Internal Efficiency,tender/tenderPeriod/endDate +Average duration of decision period days,Internal Efficiency,awards/date +Average days from award date to start of implementation,Internal Efficiency,awards/id +Average days from award date to start of implementation,Internal Efficiency,awards/date +Average days from award date to start of implementation,Internal Efficiency,awards/contractPeriod/startDate +Average days from award date to start of implementation,Internal Efficiency,contracts/period/startDate +Days between award date and tender start date,Internal Efficiency,ocid +Days between award date and tender start date,Internal Efficiency,tender/tenderPeriod/startDate +Days between award date and tender start date,Internal Efficiency,awards/date +Percent of canceled tenders to awarded tenders,Internal Efficiency,ocid +Percent of canceled tenders to awarded tenders,Internal Efficiency,tender/status +Percent of canceled tenders to awarded tenders,Internal Efficiency,awards/status +Percent of contracts which are canceled,Internal Efficiency,contracts/id +Percent of contracts which are canceled,Internal Efficiency,contracts/status +Price variation of same item across all awards,Value for Money,awards/id +Price variation of same item across all awards,Value for Money,awards/value/amount +Price variation of same item across all awards,Value for Money,awards/value/currency +Price variation of same item across all awards,Value for Money,awards/items/classification/id +Price variation of same item across all awards,Value for Money,awards/items/classification/scheme +Price variation of same item across all awards,Value for Money,awards/items/quantity +Price variation of same item across all awards,Value for Money,awards/items/unit +Percent of contracts that exceed budget,Value for Money,ocid +Percent of contracts that exceed budget,Value for Money,contracts/status +Percent of contracts that exceed budget,Value for Money,contracts/value/amount +Percent of contracts that exceed budget,Value for Money,contracts/value/currency +Percent of contracts that exceed budget,Value for Money,planning/budget/amount/amount +Percent of contracts that exceed budget,Value for Money,tender/value/amount +Percent of contracts that exceed budget,Value for Money,planning/budget/amount/currency +Percent of contracts that exceed budget,Value for Money,tender/value/currency +Mean percent overrun of contracts that exceed budget,Value for Money,ocid +Mean percent overrun of contracts that exceed budget,Value for Money,contracts/status +Mean percent overrun of contracts that exceed budget,Value for Money,contracts/value/amount +Mean percent overrun of contracts that exceed budget,Value for Money,contracts/value/currency +Mean percent overrun of contracts that exceed budget,Value for Money,planning/budget/amount/amount +Mean percent overrun of contracts that exceed budget,Value for Money,tender/value/amount +Mean percent overrun of contracts that exceed budget,Value for Money,planning/budget/amount/currency +Mean percent overrun of contracts that exceed budget,Value for Money,tender/value/currency +Total percent savings difference between budget and contract value,Value for Money,ocid +Total percent savings difference between budget and contract value,Value for Money,planning/budget/amount/amount +Total percent savings difference between budget and contract value,Value for Money,planning/budget/amount/currency +Total percent savings difference between budget and contract value,Value for Money,contracts/value/amount +Total percent savings difference between budget and contract value,Value for Money,contracts/value/currency +Total percent savings difference between tender value estimate and contract value,Value for Money,ocid +Total percent savings difference between tender value estimate and contract value,Value for Money,tender/value/amount +Total percent savings difference between tender value estimate and contract value,Value for Money,tender/value/currency +Total percent savings difference between tender value estimate and contract value,Value for Money,contracts/value/amount +Total percent savings difference between tender value estimate and contract value,Value for Money,contracts/value/currency +Percent of contracts completed on time,Value for Money,contracts/id +Percent of contracts completed on time,Value for Money,contracts/period/endDate +Percent of contracts completed on time,Value for Money,contracts/status +Share of contracts whose milestones are completed on time,Value for Money,contracts/id +Share of contracts whose milestones are completed on time,Value for Money,contracts/implementation/milestones/dueDate +Share of contracts whose milestones are completed on time,Value for Money,contracts/implementation/milestones/dateMet diff --git a/ocds_fields_indicators/main.js b/ocds_fields_indicators/main.js new file mode 100644 index 0000000..bdfac56 --- /dev/null +++ b/ocds_fields_indicators/main.js @@ -0,0 +1,560 @@ +/** + * Field ↔ Indicator Explorer (main.js) + * Purpose: Visualize dependencies between OCDS fields and indicators; selecting fields reveals computable indicators. + * Global state (window.*) tracks selection, ordering, sorting, label mode and indices built from CSV. + * Small dataset β†’ full re-render per interaction for simplicity. + */ +import { csvParse } from "https://cdn.jsdelivr.net/npm/d3-dsv@3/+esm"; + +// ------------------------------------------------------------- +// Global state containers (intentionally on window for quick, simple sharing +// across functions without a build system or state library). In a modular +// architecture you'd encapsulate these inside a closure or a state manager. +// ------------------------------------------------------------- +window._selectedFields = new Set(); +window._possibleIndicatorOrder = []; +window.fieldToIndicators = {}; +window.indicatorMap = {}; +window.usecaseMap = {}; +window._fieldSort = { key: 'count', dir: 'desc' }; +window._fieldLabelMode = 'friendly'; +// Explorer mode: 'fields-to-indicators' | 'indicators-to-fields' +window._explorerMode = 'fields-to-indicators'; +// Selected indicators (reverse mode) +window._selectedIndicators = new Set(); +// Preserve original forward-mode header markup for restoration +let _originalFieldsHeaderHTML = null; +let _originalIndicatorsHeaderHTML = null; + +const FIELD_FRIENDLY_OVERRIDES = { 'ocid': 'OCID' }; + +/** + * Convert a raw field path into a user-friendly label by removing slashes, + * expanding camelCase / underscores and capitalizing words. + * @param {string} path Raw OCDS field path. + * @returns {string} Friendly label. + */ +function friendlyFieldName(path) { + if (!path) return ''; + if (FIELD_FRIENDLY_OVERRIDES[path]) return FIELD_FRIENDLY_OVERRIDES[path]; + const words = path + .split('/') + .map(seg => seg.replace(/_/g, ' ')) + .map(seg => seg.replace(/([a-z])([A-Z])/g, '$1 $2')) + .join(' ') + .split(/\s+/) + .filter(Boolean) + .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()); + return words.join(' '); +} + +/** + * Choose label presentation (friendly vs raw path) according to global mode. + * @param {string} path Field path. + * @returns {string} Display label. + */ +function formatFieldLabel(path) { + return window._fieldLabelMode === 'friendly' ? friendlyFieldName(path) : path; +} + +// ------------------------------------------------------------- +// Fetch and initialize +// 1. Load CSV text. +// 2. Guard against accidentally serving HTML (common misconfig). +// 3. Parse with d3-dsv. +// 4. Build internal data structures, then render. +// ------------------------------------------------------------- +fetch("indicators.csv") + .then(r => { if (!r.ok) throw new Error("Could not load indicators.csv"); return r.text(); }) + .then(text => { const trimmed = text.trim(); if (trimmed.startsWith("<")) throw new Error("Loaded HTML instead of CSV – check path"); const rows = csvParse(text); buildDataStructures(rows); initialRender(); }) + .catch(err => { const f = document.getElementById("fields-list"); const i = document.getElementById("indicators-list"); if (f) f.innerHTML = `
${err.message}
`; if (i) i.innerHTML = `
${err.message}
`; console.error(err); }); + +/** + * Build in-memory indices from parsed CSV rows. + * - fieldToIndicators: field -> [indicators] + * - indicatorMap: indicator -> { usecase, fields[] } + * - usecaseMap: usecase -> [indicators] + * @param {Object[]} data Parsed CSV row objects. + * @returns {void} + */ +function buildDataStructures(data) { + const f2i = {}; + const indMap = {}; + const ucMap = {}; + data.forEach(row => { + const field = row.fields?.trim(); + const indicator = row.indicator?.trim(); + const usecase = row.usecase?.trim(); + if (!field || !indicator) return; + if (!f2i[field]) f2i[field] = new Set(); + f2i[field].add(indicator); + if (!indMap[indicator]) indMap[indicator] = { usecase: usecase || "", fields: new Set() }; + indMap[indicator].fields.add(field); + if (usecase) { + if (!ucMap[usecase]) ucMap[usecase] = new Set(); + ucMap[usecase].add(indicator); + } + }); + Object.keys(f2i).forEach(k => f2i[k] = [...f2i[k]]); + Object.keys(indMap).forEach(k => indMap[k].fields = [...indMap[k].fields]); + Object.keys(ucMap).forEach(k => ucMap[k] = [...ucMap[k]]); + window.fieldToIndicators = f2i; + window.indicatorMap = indMap; + window.usecaseMap = ucMap; +} + +/** + * Initial UI render + event listener wiring (selection, sorting, reset, label toggle). + * @returns {void} + */ +function initialRender() { + renderPanels(); + updateFieldSortIndicators(); + // Event delegation for checkboxes (avoids re-binding every render) + const fieldsList = document.getElementById("fields-list"); + fieldsList.addEventListener("change", (e) => { + if (e.target.classList.contains("field-checkbox")) { + const val = e.target.value; + if (e.target.checked) { + window._selectedFields.add(val); + } else { + window._selectedFields.delete(val); + } + renderPanels(); + } + }); + // Reset button + const resetBtn = document.getElementById("reset-fields"); + if (resetBtn) { + const runReset = () => { + if (window._explorerMode === 'fields-to-indicators') { + if (window._selectedFields.size === 0) return; + window._selectedFields.clear(); + } else { + if (window._selectedIndicators.size === 0) return; + window._selectedIndicators.clear(); + } + renderPanels(); + updateFieldSortIndicators(); + syncResetButton(); + }; + resetBtn.addEventListener("click", runReset); + } + // Sort header clicks + document.addEventListener('click', (e) => { + const btn = e.target.closest('.field-columns-header .sort-header'); + if (!btn) return; + const key = btn.getAttribute('data-sort-key'); + const state = window._fieldSort; + if (state.key === key) { + state.dir = state.dir === 'asc' ? 'desc' : 'asc'; + } else { + state.key = key; + state.dir = key === 'count' ? 'desc' : 'asc'; + } + renderPanels(); + updateFieldSortIndicators(); + }); + + // Field label mode toggle + const toggle = document.getElementById('toggle-field-label-mode'); + if (toggle) { + // Initialize attributes to reflect default mode + const friendlyInit = window._fieldLabelMode === 'friendly'; + toggle.setAttribute('aria-pressed', friendlyInit ? 'true' : 'false'); + toggle.title = friendlyInit ? 'Switch to JSON path field names' : 'Switch to friendly field names'; + toggle.setAttribute('aria-label', toggle.title); + toggle.addEventListener('click', () => { + window._fieldLabelMode = window._fieldLabelMode === 'path' ? 'friendly' : 'path'; + const friendly = window._fieldLabelMode === 'friendly'; + toggle.setAttribute('aria-pressed', friendly ? 'true' : 'false'); + toggle.title = friendly ? 'Switch to JSON path field names' : 'Switch to friendly field names'; + toggle.setAttribute('aria-label', toggle.title); + renderPanels(); + }); + } + + // Central single mode toggle + const centralToggle = document.getElementById('mode-toggle-central'); + if (centralToggle) { + const syncCentral = () => { + const mode = window._explorerMode; + if (mode === 'fields-to-indicators') { + centralToggle.dataset.mode = 'fields-to-indicators'; + centralToggle.title = 'Switch to indicators β†’ fields mode'; + centralToggle.setAttribute('aria-label', centralToggle.title); + centralToggle.innerHTML = '⇄Fields β†’ Indicators'; + } else { + centralToggle.dataset.mode = 'indicators-to-fields'; + centralToggle.title = 'Switch to fields β†’ indicators mode'; + centralToggle.setAttribute('aria-label', centralToggle.title); + centralToggle.innerHTML = '⇄Indicators β†’ Fields'; + } + }; + const applyMode = (mode) => { + if (window._explorerMode === mode) return; + window._explorerMode = mode; + // Clear opposing selection set + if (mode === 'fields-to-indicators') { + window._selectedIndicators.clear(); + } else { + window._selectedFields.clear(); + } + renderPanels(); + updateFieldSortIndicators(); + syncCentral(); + }; + centralToggle.addEventListener('click', () => { + const next = window._explorerMode === 'fields-to-indicators' ? 'indicators-to-fields' : 'fields-to-indicators'; + applyMode(next); + syncResetButton(); + }); + syncCentral(); + } + syncResetButton(); +} + +/** + * Central dispatcher to render both panels based on current explorer mode. + */ +function renderPanels() { + if (window._explorerMode === 'fields-to-indicators') { + // Show field sorting header + document.querySelectorAll('#fields-panel .field-columns-header').forEach(h => h.style.display = 'flex'); + document.querySelectorAll('#indicators-panel .field-columns-header').forEach(h => h.style.display = 'flex'); + // On first load capture originals + const fieldsHeaderEl = document.querySelector('#fields-panel .field-columns-header'); + const indicatorsHeaderEl = document.querySelector('#indicators-panel .field-columns-header'); + if (fieldsHeaderEl && _originalFieldsHeaderHTML == null) _originalFieldsHeaderHTML = fieldsHeaderEl.innerHTML; + if (indicatorsHeaderEl && _originalIndicatorsHeaderHTML == null) _originalIndicatorsHeaderHTML = indicatorsHeaderEl.innerHTML; + // Restore original markup if it was overwritten by reverse mode + if (fieldsHeaderEl && _originalFieldsHeaderHTML && fieldsHeaderEl.innerHTML !== _originalFieldsHeaderHTML) { + fieldsHeaderEl.innerHTML = _originalFieldsHeaderHTML; + } + if (indicatorsHeaderEl && _originalIndicatorsHeaderHTML && indicatorsHeaderEl.innerHTML !== _originalIndicatorsHeaderHTML) { + indicatorsHeaderEl.innerHTML = _originalIndicatorsHeaderHTML; + } + const leftTitle = document.querySelector('.gh-left .gh-title'); + const rightTitle = document.querySelector('.gh-right .gh-title'); + if (leftTitle) leftTitle.firstChild && (leftTitle.childNodes[0].textContent = 'Fields'); + if (rightTitle) rightTitle.firstChild && (rightTitle.childNodes[0].textContent = 'Indicators'); + syncHeaderAccessories(); + renderFieldsPanel(); + renderIndicatorsPanel(); + } else { + // Reverse mode: repurpose headers for indicator list and required fields list + const leftHeader = document.querySelector('#fields-panel .field-columns-header'); + const rightHeader = document.querySelector('#indicators-panel .field-columns-header'); + if (leftHeader) { + leftHeader.style.display = 'flex'; + leftHeader.innerHTML = 'Indicator'; + } + if (rightHeader) { + rightHeader.style.display = 'flex'; + rightHeader.innerHTML = 'Required FieldNumber of selected indicators relying on this field'; + } + const leftTitle = document.querySelector('.gh-left .gh-title'); + const rightTitle = document.querySelector('.gh-right .gh-title'); + if (leftTitle) leftTitle.firstChild && (leftTitle.childNodes[0].textContent = 'Indicators'); + if (rightTitle) rightTitle.firstChild && (rightTitle.childNodes[0].textContent = 'Required Fields'); + syncHeaderAccessories(); + renderIndicatorSelectionPanel(); + renderRequiredFieldsPanel(); + // Restore forward headers markup lazily when switching back (handled in forward branch above by original HTML still in DOM for that mode) + } + // After rendering, ensure reset button wording matches current mode + syncResetButton(); +} + +/** + * Ensure the field label toggle button and the usecase legend icon sit under the + * correct semantic header for the current mode. + * Forward mode: Left = Fields (label toggle), Right = Indicators (legend). + * Reverse mode: Left = Indicators (legend), Right = Required Fields (label toggle removed/hidden). + */ +function syncHeaderAccessories() { + const mode = window._explorerMode; + const leftTitle = document.querySelector('.gh-left .gh-title'); + const rightTitle = document.querySelector('.gh-right .gh-title'); + const labelToggle = document.getElementById('toggle-field-label-mode'); + const legend = document.querySelector('.usecase-help'); + if (!leftTitle || !rightTitle) return; + // Clear accidental duplicates (DOM move, not clone) + if (mode === 'fields-to-indicators') { + // label toggle β†’ left, legend β†’ right + if (labelToggle && !leftTitle.contains(labelToggle)) leftTitle.appendChild(labelToggle); + if (legend && !rightTitle.contains(legend)) rightTitle.appendChild(legend); + if (labelToggle) labelToggle.style.display = 'inline-flex'; + } else { + // legend β†’ left, label toggle logically belongs with fields but right header now shows required fields (no toggle) + if (legend && !leftTitle.contains(legend)) leftTitle.appendChild(legend); + if (labelToggle) { + // For reverse mode the label toggle still affects field labels in required fields list, keep it in right header for utility + if (!rightTitle.contains(labelToggle)) rightTitle.appendChild(labelToggle); + labelToggle.style.display = 'inline-flex'; + } + } +} + +/** Update reset button aria-label/title according to active mode */ +function syncResetButton() { + const btn = document.getElementById('reset-fields'); + if (!btn) return; + const mode = window._explorerMode; + if (mode === 'fields-to-indicators') { + btn.title = 'Reset selected fields'; + btn.setAttribute('aria-label', 'Reset selected fields'); + } else { + btn.title = 'Reset selected indicators'; + btn.setAttribute('aria-label', 'Reset selected indicators'); + } +} + +/** Reverse mode LEFT panel: indicator selection list */ +function renderIndicatorSelectionPanel() { + const list = document.getElementById('fields-list'); // reuse left pane container + const statsDiv = document.getElementById('fields-stats'); + if (!list) return; + list.innerHTML = ''; + const indicatorMap = window.indicatorMap; + const selectedIndicators = window._selectedIndicators; + const entries = Object.entries(indicatorMap).sort((a, b) => { + const ua = (a[1].usecase || '').toLowerCase(); + const ub = (b[1].usecase || '').toLowerCase(); + if (ua && ub) { + if (ua !== ub) return ua < ub ? -1 : 1; + } else if (ua && !ub) { + return -1; // defined usecase before empty + } else if (!ua && ub) { + return 1; + } + return a[0].localeCompare(b[0]); + }); + const usecaseIcons = { + "Market Opportunity": "πŸ’Ό", + "Public Integrity": "πŸ•΅οΈ", + "Service Delivery": "🚚", + "Internal Efficiency": "βš™οΈ", + "Value for Money": "πŸ’°" + }; + entries.forEach(([name, obj]) => { + const div = document.createElement('div'); + div.className = 'indicator-select-item'; + const checked = selectedIndicators.has(name); + div.innerHTML = ` + `; + list.appendChild(div); + }); + if (statsDiv) statsDiv.textContent = `${selectedIndicators.size} of ${entries.length} indicators selected`; + // Delegate checkbox events (similar pattern) – ensure only one listener attached + if (!list._indicatorSelectionBound) { + list.addEventListener('change', (e) => { + if (e.target.classList.contains('indicator-checkbox')) { + const val = e.target.value; + if (e.target.checked) selectedIndicators.add(val); else selectedIndicators.delete(val); + // Immediate visual update for selection highlight + const row = e.target.closest('.indicator-select-row'); + if (row) row.classList.toggle('is-selected', e.target.checked); + renderRequiredFieldsPanel(); + const statsDiv2 = document.getElementById('fields-stats'); + if (statsDiv2) statsDiv2.textContent = `${selectedIndicators.size} of ${entries.length} indicators selected`; + } + }); + list._indicatorSelectionBound = true; + } +} + +/** Reverse mode RIGHT panel: aggregated required fields */ +function renderRequiredFieldsPanel() { + const container = document.getElementById('indicators-list'); + const statsDiv = document.getElementById('indicators-stats'); + if (!container) return; + container.innerHTML = ''; + const indicatorMap = window.indicatorMap; + const selectedIndicators = window._selectedIndicators; + if (selectedIndicators.size === 0) { + if (statsDiv) statsDiv.textContent = '0 required fields'; + container.innerHTML = '
Select indicators to see required fields.
'; + return; + } + const freq = new Map(); + selectedIndicators.forEach(ind => { + const obj = indicatorMap[ind]; + if (!obj) return; + obj.fields.forEach(f => { + freq.set(f, (freq.get(f) || 0) + 1); + }); + }); + const rows = [...freq.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); + const max = rows[0] ? rows[0][1] : 1; + const listDiv = document.createElement('div'); + listDiv.className = 'required-fields-list'; + rows.forEach(([field, count]) => { + const pct = (count / max) * 100; + const wrap = document.createElement('div'); + wrap.className = 'field-item'; + wrap.innerHTML = ` +
+ ${formatFieldLabel(field)} +
+ ${count} + `; + listDiv.appendChild(wrap); + }); + container.appendChild(listDiv); + if (statsDiv) statsDiv.textContent = `${rows.length} required field${rows.length !== 1 ? 's' : ''}`; +} + +/** + * Render the fields panel: stats + sorted list + proportional width bars. + * @returns {void} + */ +function renderFieldsPanel() { + const fieldToIndicators = window.fieldToIndicators; + const selectedFields = window._selectedFields; + const sortState = window._fieldSort || { key: 'count', dir: 'desc' }; + const fieldsList = document.getElementById("fields-list"); + if (!fieldsList) return; + fieldsList.innerHTML = ""; + const totalFields = Object.keys(fieldToIndicators).length; + const statsDiv = document.getElementById("fields-stats"); + if (statsDiv) statsDiv.textContent = `${selectedFields.size} of ${totalFields} fields available`; + const entries = Object.entries(fieldToIndicators); + const globalMax = entries.reduce((m, [, arr]) => Math.max(m, arr.length), 0) || 1; + let sorted = [...entries]; + if (sortState.key === 'count') { + sorted.sort((a, b) => a[1].length - b[1].length); + } else if (sortState.key === 'field') { + sorted.sort((a, b) => a[0].localeCompare(b[0])); + } + if (sortState.dir === 'desc') sorted.reverse(); + const max = globalMax; + sorted.forEach(([field, indicators]) => { + const count = indicators.length; + let barWidth = (count / max) * 100; + const isTiny = barWidth > 0 && barWidth < 0.8 && count < max; + const wrap = document.createElement("div"); + wrap.className = "field-item"; + wrap.innerHTML = ` + + `; + fieldsList.appendChild(wrap); + }); +} + +/** + * Render indicators: computable (stable emergence order) then non-computable (by completion ratio + name). + * @returns {void} + */ +function renderIndicatorsPanel() { + const indicatorMap = window.indicatorMap; + const selectedFields = window._selectedFields; + const indicatorsList = document.getElementById("indicators-list"); + if (!indicatorsList) return; + indicatorsList.innerHTML = ""; + const usecaseIcons = { + "Market Opportunity": "πŸ’Ό", + "Public Integrity": "πŸ•΅οΈ", + "Service Delivery": "🚚", + "Internal Efficiency": "βš™οΈ", + "Value for Money": "πŸ’°" + }; + + const all = Object.entries(indicatorMap).map(([name, obj]) => ({ + name, + usecase: obj.usecase, + fields: obj.fields, + can: obj.fields.every(f => selectedFields.has(f)), + satisfied: obj.fields.filter(f => selectedFields.has(f)), + missing: obj.fields.filter(f => !selectedFields.has(f)) + })); + if (!window._possibleIndicatorOrder) window._possibleIndicatorOrder = []; + const order = window._possibleIndicatorOrder; + const currentPossible = all.filter(i => i.can).map(i => i.name); + for (let i = order.length - 1; i >= 0; i--) if (!currentPossible.includes(order[i])) order.splice(i, 1); + currentPossible.forEach(name => { if (!order.includes(name)) order.push(name); }); + const possible = order.map(n => all.find(i => i.name === n)).filter(Boolean); + const notPossible = all.filter(i => !i.can) + .sort((a, b) => { + const ar = a.satisfied.length / a.fields.length; + const br = b.satisfied.length / b.fields.length; + if (br !== ar) return br - ar; + return a.name.localeCompare(b.name); + }); + const statsDiv = document.getElementById("indicators-stats"); + if (statsDiv) statsDiv.textContent = `${possible.length} of ${all.length} indicators can be calculated`; + const listDiv = document.createElement("div"); + listDiv.className = "indicator-list"; + const renderIndicator = (ind, possibleFlag) => { + const div = document.createElement("div"); + div.className = `indicator-item ${possibleFlag ? "indicator-possible" : "indicator-disabled"}`; + const progressLabel = possibleFlag ? "βœ”" : `${ind.satisfied.length}/${ind.fields.length}`; + let missingFieldsHtml = ""; + if (!possibleFlag && ind.missing.length > 0) { + missingFieldsHtml = `

Missing: ${ind.missing.map(f => formatFieldLabel(f)).join(", ")}

`; + } + if (possibleFlag) { + div.innerHTML = ` +

+ ${usecaseIcons[ind.usecase] || "❓"} + ${ind.name} + ${progressLabel} +

+ `; + } else { + div.innerHTML = ` +

+ ${usecaseIcons[ind.usecase] || "❓"} + ${ind.name} + ${progressLabel} +

+ ${missingFieldsHtml} + `; + } + listDiv.appendChild(div); + }; + possible.forEach(ind => renderIndicator(ind, true)); + notPossible.forEach(ind => renderIndicator(ind, false)); + indicatorsList.appendChild(listDiv); +} + +/** + * Update visual sort directional indicators in header. + * @returns {void} + */ +function updateFieldSortIndicators() { + const sortState = window._fieldSort; + document.querySelectorAll('.field-columns-header .sort-indicator').forEach(el => { + const key = el.getAttribute('data-key'); + el.classList.remove('sort-asc', 'sort-desc'); + if (key === sortState.key) { + el.classList.add(sortState.dir === 'asc' ? 'sort-asc' : 'sort-desc'); + } + }); +} + +/** + * Resolve usecase label to emoji icon. + * @param {string} usecase Usecase label. + * @returns {string} Emoji icon. + */ +function getUsecaseIcon(usecase) { + const map = { + "Market Opportunity": "πŸ’Ό", + "Public Integrity": "πŸ•΅οΈ", + "Service Delivery": "🚚", + "Internal Efficiency": "βš™οΈ", + "Value for Money": "πŸ’°" + }; + return map[usecase] || "❓"; +} diff --git a/ocds_fields_indicators/style.css b/ocds_fields_indicators/style.css new file mode 100644 index 0000000..0ee8ed6 --- /dev/null +++ b/ocds_fields_indicators/style.css @@ -0,0 +1,597 @@ +.indicator-missing-fields-block { + display: block; + font-size: 10px; + color: #999; + margin: 4px 0 0 calc(22px + 8px); + /* align with indicator-name after icon */ + font-style: italic; + word-break: break-word; +} + +/* Mode toggle */ +.mode-toggle .mode-btn { + border: 1px solid #ccc; + background: #f6f6f6; + color: #333; + padding: 4px 10px; + font-size: 11px; + letter-spacing: .5px; + text-transform: uppercase; + cursor: pointer; + border-radius: 4px; + font-family: inherit; + transition: background .15s, color .15s, box-shadow .15s; +} + +.mode-toggle .mode-btn:not(.active):hover { + background: #e8e8e8; +} + +.mode-toggle .mode-btn.active { + background: #1f2933; + color: #fff; + box-shadow: 0 0 0 1px #1f2933; +} + +.mode-toggle .mode-btn:focus { + outline: none; + box-shadow: 0 0 0 2px #90caf9; +} + +/* Central single mode toggle button */ +.mode-toggle-central { + position: absolute; + top: 6px; + left: 50%; + transform: translate(-50%, 0); + z-index: 250; + /* above header content */ + border: 1px solid #d0d0d0; + background: linear-gradient(#fafafa, #f0f0f0); + color: #1f2933; + padding: 6px 10px 7px; + font-size: 14px; + line-height: 1; + border-radius: 24px; + cursor: pointer; + font-family: inherit; + display: inline-flex; + align-items: center; + gap: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12); + transition: background .25s, box-shadow .25s, transform .35s cubic-bezier(.4, 1.6, .4, 1), color .25s; +} + +.mode-toggle-central:hover { + background: linear-gradient(#fff, #f5f5f5); +} + +.mode-toggle-central:focus { + outline: none; + box-shadow: 0 0 0 2px #90caf9; +} + +.mode-toggle-central:active { + transform: translate(-50%, 1px); +} + +.mode-toggle-central[data-mode="indicators-to-fields"] { + color: #fff; + background: linear-gradient(#1f2933, #111b21); + border-color: #1f2933; +} + +.mode-toggle-central[data-mode="indicators-to-fields"]:hover { + background: linear-gradient(#263341, #182028); +} + +.mode-toggle-central .mode-toggle-icon { + font-size: 16px; +} + +.mode-toggle-central .mode-toggle-text { + font-size: 11px; + letter-spacing: .5px; + text-transform: uppercase; + font-weight: 600; +} + +/* Provide layout context for absolute centered toggle */ +.global-header { + position: sticky; +} + +.indicator-main-row { + display: flex; + align-items: center; +} + +.indicator-missing-fields-row { + display: none; +} + +/* remove old variants; paragraph block handles styling */ +html, +body { + background: #ffffff; + color: #222; + margin: 0; + padding: 0; +} + +/* Usecase icon legend tooltip */ +.usecase-help { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + font-size: 13px; + cursor: help; + border-radius: 4px; + background: #f0f0f0; + color: #444; + user-select: none; +} + +/* Icon button (reset) */ +.icon-btn { + border: none; + background: #f0f0f0; + color: #444; + padding: 2px 8px 3px; + line-height: 1; + font-size: 14px; + cursor: pointer; + border-radius: 6px; + font-family: inherit; + transition: background .15s, transform .15s; +} + +.icon-btn:hover, +.icon-btn:focus { + background: #e2e2e2; + outline: none; +} + +.icon-btn:active { + transform: translateY(1px); +} + +.icon-btn[aria-pressed="true"] { + background: #1f2933; + color: #fff; +} + +.usecase-help:focus, +.usecase-help:hover { + background: #e2e2e2; +} + +.usecase-help .tooltip { + position: absolute; + top: 28px; + left: 0; + z-index: 120; + /* above .field-columns-header (80) and global header (50) */ + background: #1f2933; + color: #fff; + padding: 10px 12px; + font-size: 12px; + line-height: 1.3; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 190px; + display: none; + pointer-events: none; +} + +.usecase-help:hover .tooltip, +.usecase-help:focus .tooltip { + display: block; +} + +.usecase-help .tooltip strong { + display: block; + font-weight: 600; + margin-bottom: 4px; +} + +.field-header-row { + display: flex; + align-items: center; + font-size: 0.85em; + color: #888; + margin-bottom: 4px; + width: 100%; +} + +.field-header-name { + flex: 1; + margin-left: 32px; +} + +.field-header-count { + min-width: 32px; + text-align: right; + border-left: 1px solid #eee; + padding-left: 8px; + margin-left: 8px; +} + +.field-count-header { + display: flex; + align-items: center; + justify-content: flex-end; + font-size: 0.85em; + color: #888; + margin-bottom: 4px; + width: 100%; +} + +.field-count { + background: none; + font-size: 0.9em; + color: #555; + min-width: 32px; + text-align: right; + flex-shrink: 0; + border-left: 1px solid #eee; + padding-left: 8px; + margin-left: 8px; + position: static; + top: auto; + right: auto; + transform: none; +} + +#container { + display: flex; + flex-direction: column; + height: 100vh; + font-family: 'Fira Mono', 'Ubuntu Mono', monospace; + overflow: hidden; +} + +.global-header { + position: sticky; + top: 0; + z-index: 200; + /* raised above .field-columns-header (80) so tooltip appears on top */ + display: flex; + justify-content: space-between; + gap: 32px; + padding: 14px 24px 10px 24px; + background: #fff; + box-shadow: 0 4px 10px -6px rgba(0, 0, 0, 0.18); + border-bottom: 1px solid #eee; +} + +.gh-left, +.gh-right { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.gh-title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.gh-stats { + font-size: 12px; + color: #555; + background: #f5f5f5; + padding: 4px 8px; + border-radius: 4px; +} + +.field-columns-header { + display: flex; + align-items: center; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #666; + padding: 10px 6px 10px 6px; + /* uniform padding */ + border-bottom: 1px solid #e2e2e2; + margin-bottom: 6px; + /* tighter gap */ + position: sticky; + top: 0; + /* inside scrollable panel body */ + background: #fff; + z-index: 80; + /* above field items */ + box-shadow: 0 2px 4px -2px rgba(0, 0, 0, 0.08); +} + +.field-columns-header::after { + content: ""; + position: absolute; + inset: 0; + background: #fff; + /* ensure full opaque backdrop */ + z-index: -1; +} + +/* Ensure list content does not visually tuck underneath while scrolling */ +#fields-list { + scroll-margin-top: 44px; +} + +/* Field items below the header */ +.field-item { + position: relative; +} + +.field-columns-header .col-field { + flex: 1; + padding-left: 30px; +} + +/* Ensure the clickable sort button for Field inherits same left padding */ +.field-columns-header .col-field.sort-header { + padding-left: 25px; +} + +.field-columns-header .col-count { + min-width: 170px; + text-align: right; +} + +/* Reverse mode header specific columns */ +.field-columns-header .col-indicator-rev { + flex: 1; + /* Checkbox (~16px) + checkbox margin (8px) + usecase icon width (22px) + icon margin-right (8px) = 54px. + Original field header uses 25px because only checkbox + small gap precede label. Here we need larger offset. */ + padding-left: 65px; +} + +.field-columns-header .reverse-required-field { + padding-left: 0 !important; + margin-left: 0; +} + +.field-columns-header .sort-header { + background: none; + border: none; + font: inherit; + color: inherit; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.field-columns-header .sort-header:focus { + outline: none; +} + +.field-columns-header .sort-header:hover { + color: #222; +} + +.sort-indicator { + width: 8px; + display: inline-block; +} + +.sort-indicator::before { + content: ''; + display: block; + width: 0; + height: 0; + margin: 0 auto; +} + +.sort-indicator.sort-asc::before { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 6px solid #444; +} + +.sort-indicator.sort-desc::before { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #444; +} + +.panels-wrapper { + flex: 1; + display: grid; + grid-template-columns: 1fr 1fr; + overflow: hidden; +} + +#fields-panel, +#indicators-panel { + position: relative; + overflow-y: auto; + padding: 20px 24px 40px 24px; + border-right: 1px solid #eee; +} + +#indicators-panel { + border-right: none; +} + +.field-item { + display: flex; + align-items: center; + margin-bottom: 8px; + width: 100%; + position: relative; + min-height: 32px; +} + +.field-bar { + height: 28px; + background: #e0e0e0; + border-radius: 14px; + display: flex; + align-items: center; + transition: background 0.2s; + min-width: 0; + max-width: 100%; + position: relative; + overflow: visible; +} + +.field-bar.checked { + background: #90caf9; +} + +.field-bar-micro { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset; +} + +.field-label { + margin-left: 8px; + font-weight: 500; + white-space: nowrap; + overflow: visible; + text-overflow: initial; +} + +.field-count { + background: none; + font-size: 0.9em; + color: #555; + min-width: 24px; + text-align: right; + flex-shrink: 0; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.indicator-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.indicator-item { + display: block; + padding: 6px 12px; + border-radius: 8px; + background: #f7f7f7; + font-size: 1em; + transition: background 0.7s cubic-bezier(.4, 2, .3, 1), color 0.7s cubic-bezier(.4, 2, .3, 1), transform 0.9s cubic-bezier(.4, 2, .3, 1); + margin-bottom: 2px; + position: relative; +} + +/* Reverse mode: indicator selection list left side */ +.indicator-select-item { + margin: 0 0 4px 0; +} + +.indicator-select-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 8px; + background: #f7f7f7; + cursor: pointer; + font-size: 1em; + /* match forward mode indicator font size */ + position: relative; + transition: background .25s, box-shadow .25s, transform .25s; +} + +.indicator-select-row:hover { + background: #ececec; +} + +.indicator-select-row.is-selected { + background: #c8e6c9; + /* same as .indicator-possible */ + box-shadow: 0 0 0 1px #81c784 inset; + transform: translateY(-2px); + /* subtle lift similar to forward mode emergence */ +} + +.indicator-select-row input[type="checkbox"] { + margin: 0 2px 0 0; +} + +.indicator-select-row .indicator-name { + font-weight: 500; +} + +.indicator-select-row.is-selected .indicator-name { + font-weight: 600; +} + + +.indicator-head-row { + display: flex; + align-items: center; + margin: 0; + padding: 0; +} + +.indicator-possible { + background: #c8e6c9; + color: #222; + font-weight: 500; + transform: translateY(-8px) scale(1.03); + z-index: 1; +} + +.indicator-disabled { + color: #777; + background: #f9f9f9; + font-weight: 400; + transform: none; +} + +.indicator-usecase { + width: 22px; + /* fixed width for alignment */ + margin-right: 8px; + font-size: 1.2em; + display: inline-flex; + justify-content: center; +} + +.indicator-progress { + margin-left: auto; + font-size: 11px; + background: #e0e0e0; + padding: 2px 6px; + border-radius: 12px; + line-height: 1; + font-weight: 600; + color: #333; + font-family: 'Fira Mono', 'Ubuntu Mono', monospace; +} + +.indicator-item.indicator-possible .indicator-progress { + background: #4caf50; + color: #fff; +} + +.indicator-missing-fields { + font-size: 10px; + color: #999; + margin-left: 8px; + font-style: italic; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 120px; + vertical-align: middle; +} \ No newline at end of file