@rcsb/rcsb-statistics 0.3.44-dev.aipcb.4 → 0.3.44-dev.aipcb.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ai/chart-catalog.json +7 -4
- package/build/ai/chart-core.d.ts +9 -4
- package/build/ai/chart-core.js +1 -1
- package/build/index.html +1 -1
- package/build/static/css/{221.efddfa04.chunk.css → 221.4d4636e5.chunk.css} +5 -1
- package/build/static/css/{489.886871bf.chunk.css → 489.3a5def3c.chunk.css} +5 -1
- package/build/static/js/489.676a8551.chunk.js +1 -0
- package/build/static/js/742.1ba59bb6.chunk.js +1 -0
- package/build/static/js/{main.4341a6ec.js → main.734f05c1.js} +2 -2
- package/package.json +1 -1
- package/build/static/js/417.a91bf47f.chunk.js +0 -1
- package/build/static/js/489.0b19498e.chunk.js +0 -1
- /package/build/static/js/{main.4341a6ec.js.LICENSE.txt → main.734f05c1.js.LICENSE.txt} +0 -0
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"Percentages use all matching entries in each cohort as the denominator, including entries missing the chart attribute. They are not normalized to the displayed top categories.",
|
|
11
11
|
"All filters are entry-level requirements. An organism and ligand filter may match different entities in the same entry. Do not promise same-chain, binding-site, or same-entity relationships.",
|
|
12
12
|
"Lower and upper bounds on one numeric or date attribute are combined into one interval, preserving strict gt/lt and inclusive gte/lte boundaries. At least one value must satisfy both bounds; different reported resolutions cannot satisfy opposite ends of the interval.",
|
|
13
|
-
"Cumulative release-year counts
|
|
14
|
-
"Line, area and mixed charts require an ordered dimension, ascending labels and vertical orientation. Area series require a linear scale. Stacking requires provably disjoint cohorts
|
|
13
|
+
"Cumulative release-year counts default to the selected window. Explicit cumulativeBase archive adds a separately retrieved count before each cohort’s selected lower release-year bound, retaining every nondimension population filter. Archive cumulative mode requires count values and an explicit lower year for each cohort. Current-year counts are incomplete.",
|
|
14
|
+
"Line, area and mixed charts require an ordered dimension, ascending labels and vertical orientation. Area series require a linear scale. Stacking requires provably disjoint cohorts: non-overlapping intervals of release year, deposited molecular mass, deposited heavy-atom count, polymer monomer count, polymer-chain count or distinct polymer-entity count, or different structure determination methodologies. Strict endpoints are respected. Different organisms, methods or resolutions alone do not prove disjoint entry populations. Doughnut charts follow the same complete, non-overlapping partition rules as pie charts.",
|
|
15
15
|
"Presentation settings change appearance only. They cannot change chart counts, filters, aggregation attributes, or destinations. Use only supported palettes, hexadecimal colors and bounded drawing controls, never scripts, callbacks or plugins.",
|
|
16
16
|
"Selecting a bucket searches matching entries, keeping every global and cohort filter. The clicked bucket is an independent requirement: in a multi-valued entry, the value in the bucket may differ from the value satisfying a population filter. Resolution buckets use one half-open interval including the lower bound and excluding the upper bound.",
|
|
17
17
|
"Comparisons use up to four explicit named cohorts with independent additional filters; cohorts may overlap. Never fabricate counts or arbitrary Search API attributes.",
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"Radar compares the same measure across at most 20 categories. Polar area compares magnitudes across at most 30 categories and is not a parts-of-whole chart. Both use a linear radial scale.",
|
|
20
20
|
"Scatter and bubble use entry observations, never independently paired aggregation buckets. Each cohort selects the newest matching entries with an ID tie-break, at most 500 selected records across cohorts. This is a bounded, non-random selection, not a representative sample or an archive-wide correlation.",
|
|
21
21
|
"Missing, invalid or unavailable observation values are omitted, never replaced with zero. Resolution is the minimum positive reported combined resolution for that entry, not an average or a method-specific value. Entry-level filters need not select the same reported resolution used by a point.",
|
|
22
|
+
"Cryo-EM labels are unsupported: ELECTRON MICROSCOPY alone does not establish cryogenic specimen preparation. Clarify or use an accurately described experimental-method chart without silently substituting methods.",
|
|
23
|
+
"Release year is not an observation metric because newest-entry selection biases that axis. Use an aggregate release-year chart instead.",
|
|
22
24
|
"Bubble size is a third catalog metric from the same entry; the renderer maps it to proportional area. Raw size values and units remain available in the table and exports. No averages, medians, fitted trends or inferred scientific relationships are supported."
|
|
23
25
|
],
|
|
24
26
|
"dimensions": {
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
"polymerEntityCount": {"label": "Distinct polymer-entity count", "attribute": "rcsb_entry_info.polymer_entity_count", "kind": "number", "integer": true, "operators": ["gte", "lte", "gt", "lt"], "minimum": 0, "maximum": 1000000000},
|
|
47
49
|
"sourceTaxonomy": {"label": "Source organism NCBI taxonomy ID (including descendants)", "attribute": "rcsb_entity_source_organism.taxonomy_lineage.id", "kind": "taxonomy", "operators": ["in"]},
|
|
48
50
|
"sourceOrganism": {"label": "Exact source organism scientific name", "attribute": "rcsb_entity_source_organism.ncbi_scientific_name", "kind": "text", "operators": ["in"]},
|
|
49
|
-
"ligand": {"label": "Non-polymer chemical component ID present in entry", "attribute": "
|
|
51
|
+
"ligand": {"label": "Non-polymer chemical component ID present in entry", "attribute": "rcsb_nonpolymer_entity_container_identifiers.nonpolymer_comp_id", "kind": "identifier", "operators": ["in"]},
|
|
50
52
|
"polymerType": {"label": "Polymer type present in entry", "attribute": "entity_poly.rcsb_entity_polymer_type", "kind": "enum", "operators": ["in"], "values": ["Protein", "DNA", "RNA", "NA-hybrid", "Other"]},
|
|
51
53
|
"methodology": {"label": "Structure determination methodology", "attribute": "rcsb_entry_info.structure_determination_methodology", "kind": "enum", "operators": ["in"], "values": ["experimental", "integrative"]}
|
|
52
54
|
},
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
"topN": {"type": "integer", "minimum": 1, "maximum": 60},
|
|
79
81
|
"interval": {"type": "number", "minimum": 0.1, "maximum": 100000},
|
|
80
82
|
"cumulative": {"type": "boolean"},
|
|
83
|
+
"cumulativeBase": {"type": "string", "enum": ["window", "archive"]},
|
|
81
84
|
"presentation": {"$ref": "#/$defs/presentation"},
|
|
82
85
|
"observations": {"anyOf": [{"type": "null"}, {"type": "object", "additionalProperties": false, "required": ["x", "y", "size", "limit"], "properties": {
|
|
83
86
|
"x": {"$ref": "#/$defs/metric"}, "y": {"$ref": "#/$defs/metric"}, "size": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/metric"}]}, "limit": {"type": "integer", "minimum": 1, "maximum": 500}
|
|
@@ -86,7 +89,7 @@
|
|
|
86
89
|
"series": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "object", "additionalProperties": false, "required": ["label", "filters"], "properties": {"label": {"type": "string", "minLength": 1, "maxLength": 80}, "filters": {"type": "array", "maxItems": 8, "items": {"$ref": "#/$defs/filter"}}}}}
|
|
87
90
|
},
|
|
88
91
|
"$defs": {
|
|
89
|
-
"metric": {"type": "string", "enum": ["resolution", "molecularWeight", "atomCount", "monomerCount", "chainCount", "polymerEntityCount"
|
|
92
|
+
"metric": {"type": "string", "enum": ["resolution", "molecularWeight", "atomCount", "monomerCount", "chainCount", "polymerEntityCount"]},
|
|
90
93
|
"axisBound": {"anyOf": [{"type": "number", "minimum": -1000000000000, "maximum": 1000000000000}, {"type": "null"}]},
|
|
91
94
|
"presentation": {"type": "object", "additionalProperties": false, "properties": {
|
|
92
95
|
"palette": {"type": "string", "enum": ["default", "accessible", "warm", "cool", "monochrome"]},
|
package/build/ai/chart-core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface ChartFilter { field: string; operator: 'in' | 'gte' | 'lte' | 'gt' | 'lt'; value: number | Array<string | number>; }
|
|
2
|
-
export type ChartMetric = 'resolution' | 'molecularWeight' | 'atomCount' | 'monomerCount' | 'chainCount' | 'polymerEntityCount'
|
|
2
|
+
export type ChartMetric = 'resolution' | 'molecularWeight' | 'atomCount' | 'monomerCount' | 'chainCount' | 'polymerEntityCount';
|
|
3
3
|
export interface ChartObservation { x: ChartMetric; y: ChartMetric; size: ChartMetric | null; limit: number; }
|
|
4
4
|
export interface ChartPoint { entryId: string; seriesIndex: number; x: number; y: number; size?: number; }
|
|
5
5
|
export interface ChartPresentation {
|
|
@@ -44,6 +44,7 @@ export interface ChartSpec {
|
|
|
44
44
|
topN: number;
|
|
45
45
|
interval: number;
|
|
46
46
|
cumulative: boolean;
|
|
47
|
+
cumulativeBase?: 'window' | 'archive';
|
|
47
48
|
presentation?: Partial<ChartPresentation>;
|
|
48
49
|
observations?: ChartObservation | null;
|
|
49
50
|
filters: ChartFilter[];
|
|
@@ -51,10 +52,10 @@ export interface ChartSpec {
|
|
|
51
52
|
}
|
|
52
53
|
export interface ChartDataResult {
|
|
53
54
|
kind?: 'records';
|
|
54
|
-
rows: Array<{ label: string; counts: number[]; values: number[] }>;
|
|
55
|
+
rows: Array<{ label: string; counts: number[]; values: number[]; partial?: boolean }>;
|
|
55
56
|
points?: ChartPoint[];
|
|
56
57
|
selectedEntryIds?: string[][];
|
|
57
|
-
series: Array<{ label: string; total: number; selected?: number; omitted?: number }>;
|
|
58
|
+
series: Array<{ label: string; total: number; windowTotal?: number; baselineCount?: number; selected?: number; omitted?: number }>;
|
|
58
59
|
warnings: string[];
|
|
59
60
|
retrievedAt: string;
|
|
60
61
|
categoryCount: number;
|
|
@@ -75,10 +76,14 @@ export function buildObservationRequests(input: ChartSpec): Array<Record<string,
|
|
|
75
76
|
export function buildObservationDataRequest(entryIds: string[]): { query: string; variables: { ids: string[] } };
|
|
76
77
|
export function formatObservationData(input: ChartSpec, selections: unknown[], entries: unknown[]): ChartDataResult;
|
|
77
78
|
export function buildChartRequests(input: ChartSpec): Array<Record<string, unknown>>;
|
|
79
|
+
export function buildChartBaselineRequests(input: ChartSpec): Array<Record<string, unknown>>;
|
|
80
|
+
export function estimateChartRequests(input: ChartSpec): { aggregateRequests: number; baselineRequests: number; selectionRequests: number; dataRequests: number; totalRequests: number };
|
|
81
|
+
export function chartBinBounds(input: ChartSpec, label: string): { lower: number; upperExclusive: number } | null;
|
|
82
|
+
export function formatChartBucketLabel(input: ChartSpec, label: string, partial?: boolean): string;
|
|
78
83
|
export function getChartPresentation(input: ChartSpec): ChartPresentation;
|
|
79
84
|
export function buildChartDrilldownRequest(input: ChartSpec, rowLabel: string, seriesIndex: number): Record<string, unknown>;
|
|
80
85
|
export function describeFilter(input: ChartFilter): string;
|
|
81
86
|
export function describeChart(input: ChartSpec): string;
|
|
82
87
|
export function chartWarnings(input: ChartSpec): string[];
|
|
83
88
|
export function chartChanges(previous: ChartSpec | null, input: ChartSpec): string[];
|
|
84
|
-
export function formatChartData(input: ChartSpec, responses: unknown[]): ChartDataResult;
|
|
89
|
+
export function formatChartData(input: ChartSpec, responses: unknown[], baselineResponses?: unknown[], options?: { retrievedAt?: string }): ChartDataResult;
|
package/build/ai/chart-core.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const catalog=require("./chart-catalog.json"),isObject=e=>Boolean(e&&"object"==typeof e&&!Array.isArray(e)),has=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),fail=e=>{throw new Error(e)};function validateSchema(e,t,r=t,a="Chart"){if(t.$ref)return validateSchema(e,r.$defs[t.$ref.split("/").pop()],r,a);if(t.anyOf)return void(t.anyOf.some(t=>{try{return validateSchema(e,t,r,a),!0}catch{return!1}})||fail(`${a} has an invalid value.`));const i=t.type;("object"===i&&!isObject(e)||"array"===i&&!Array.isArray(e)||"integer"===i&&!Number.isInteger(e)||"number"===i&&("number"!=typeof e||!Number.isFinite(e))||"string"===i&&"string"!=typeof e||"boolean"===i&&"boolean"!=typeof e||"null"===i&&null!==e)&&fail(`${a} must be ${i}.`),t.enum&&!t.enum.includes(e)&&fail(`${a} is not supported.`),"number"==typeof e&&(void 0!==t.minimum&&e<t.minimum||void 0!==t.maximum&&e>t.maximum)&&fail(`${a} is outside the supported range.`),"string"==typeof e&&(t.minLength&&e.trim().length<t.minLength||t.maxLength&&e.length>t.maxLength||/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(e))&&fail(`${a} has invalid text.`),"string"==typeof e&&t.pattern&&!new RegExp(t.pattern).test(e)&&fail(`${a} has invalid text.`),Array.isArray(e)&&((t.minItems&&e.length<t.minItems||t.maxItems&&e.length>t.maxItems||t.uniqueItems&&new Set(e.map(e=>JSON.stringify(e))).size!==e.length)&&fail(`${a} has an invalid number of items.`),e.forEach((e,i)=>validateSchema(e,t.items,r,`${a}[${i}]`))),isObject(e)&&((t.required||[]).forEach(t=>{has(e,t)||fail(`${a}.${t} is required.`)}),Object.keys(e).forEach(i=>{has(t.properties||{},i)?validateSchema(e[i],t.properties[i],r,`${a}.${i}`):!1===t.additionalProperties&&fail(`${a}.${i} is not supported.`)}))}const isObservationChart=e=>["scatter","bubble"].includes(e.chartType),chartMetricLabel=e=>{has(catalog.metrics,e)||fail("Select a supported observation metric.");const{label:t,unit:r}=catalog.metrics[e];return`${t} (${r})`};function numericBounds(e,t){let r=-1/0,a=1/0,i=!0,n=!0;return e.filter(e=>e.field===t).forEach(e=>{const s=["gte","gt"].includes(e.operator),o=["gt","lt"].includes(e.operator),l=e.value+("releaseYear"===t&&o?s?1:-1:0),u="releaseYear"===t||!o;s&&(l>r||l===r&&!u)&&(r=l,i=u),!s&&(l<a||l===a&&!u)&&(a=l,n=u)}),{from:r,to:a,include_lower:i,include_upper:n}}function disjointCohorts(e){const t=e.series.map(t=>[...e.filters,...t.filters]),r=e=>e.filter(e=>"methodology"===e.field).reduce((e,t)=>e.filter(e=>t.value.includes(e)),catalog.filters.methodology.values);return t.every((e,a)=>t.slice(a+1).every(t=>{if(!r(e).some(e=>r(t).includes(e)))return!0;const a=numericBounds(e,"releaseYear"),i=numericBounds(t,"releaseYear");return a.to<i.from||i.to<a.from}))}function validateChartSpec(e){validateSchema(e,catalog.specSchema);const t=JSON.parse(JSON.stringify(e)),r=catalog.dimensions[t.dimension],a=isObservationChart(t),i={...catalog.presentationDefaults,...t.presentation};!["line","area","mixed"].includes(t.chartType)||r.ordered&&"vertical"===t.orientation&&"labelAsc"===t.sort||fail("Line, area and mixed charts require an ordered year or numeric dimension, vertical orientation, and ascending labels."),a||!r.integer||Number.isInteger(t.interval)||fail("Count histograms require a whole-number bin width of at least 1."),("area"===t.chartType||"mixed"===t.chartType&&i.seriesTypes.includes("area"))&&"linear"!==t.scale&&fail("Area charts require a linear scale for their zero baseline."),["pie","doughnut"].includes(t.chartType)&&(r.overlapping||1!==t.series.length||t.cumulative||"linear"!==t.scale)&&fail("Pie and doughnut charts require a single non-overlapping series, linear scale, and non-cumulative values."),["radar","polarArea"].includes(t.chartType)&&("linear"!==t.scale||t.cumulative||t.topN>("radar"===t.chartType?20:30))&&fail("Radar and polar-area charts require a linear scale, non-cumulative values and a bounded category limit (20 for radar, 30 for polar area)."),"polarArea"===t.chartType&&1!==t.series.length&&fail("Polar-area charts require one series. Use radar or grouped bars to compare cohorts."),"mixed"===t.chartType?(i.seriesTypes.length!==t.series.length||new Set(i.seriesTypes).size<2)&&fail("Mixed charts require a drawing type for every series and at least two different types."):i.seriesTypes.length&&fail("Per-series drawing types are only supported for mixed charts."),a?(t.observations||fail("Scatter and bubble charts require paired entry observations."),t.observations.limit*t.series.length>500&&fail("Select at most 500 entry observations across all cohorts."),("count"!==t.measure||t.cumulative||"vertical"!==t.orientation)&&fail("Observation charts require count mode, vertical orientation and non-cumulative values."),("bubble"===t.chartType&&!t.observations.size||"scatter"===t.chartType&&null!==t.observations.size)&&fail("Bubble charts require a size metric; scatter charts require a null size metric.")):null!=t.observations&&fail("Entry observations are only supported for scatter and bubble charts."),a||"linear"===i.xScale&&null===i.xMin&&null===i.xMax||fail("Numeric X-axis scales and bounds are only supported for observation charts.");for(const e of["x","y"]){const r=i[`${e}Min`],a=i[`${e}Max`];null!==r&&null!==a&&r>=a&&fail("An axis minimum must be less than its maximum."),"logarithmic"===("x"===e?i.xScale:t.scale)&&[r,a].some(e=>null!==e&&e<=0)&&fail("Logarithmic axis bounds must be positive.")}return!t.cumulative||"releaseYear"===t.dimension&&"labelAsc"===t.sort||fail("Cumulative charts require release year in chronological order."),"percent"===t.measure&&"linear"!==t.scale&&fail("Percentages require a linear scale."),new Set(t.series.map(e=>e.label)).size!==t.series.length&&fail("Give each comparison series a different label."),[t.filters,...t.series.map(e=>e.filters)].forEach(e=>e.forEach(e=>{const t=catalog.filters[e.field];t.operators.includes(e.operator)||fail(`Unsupported operator for ${t.label}.`),"in"===e.operator?(Array.isArray(e.value)||fail(`${t.label} requires a list.`),e.value.forEach(e=>{"taxonomy"===t.kind?(!Number.isInteger(e)||e<=0)&&fail("Taxonomy IDs must be positive integers."):("string"!=typeof e||t.values&&!t.values.includes(e)||"identifier"===t.kind&&!/^[A-Z0-9]{1,10}$/.test(e))&&fail(`Invalid value for ${t.label}.`)})):("number"!=typeof e.value||e.value<t.minimum||e.value>t.maximum||("year"===t.kind||t.integer)&&!Number.isInteger(e.value))&&fail(`Invalid range for ${t.label}.`)})),t.series.forEach(e=>{const r=[...t.filters,...e.filters];Object.keys(catalog.filters).filter(e=>["number","year"].includes(catalog.filters[e].kind)).forEach(e=>{const t=numericBounds(r,e);(t.from>t.to||t.from===t.to&&(!t.include_lower||!t.include_upper))&&fail(`The ${e} lower bound exceeds its upper bound or defines an empty interval.`)}),!a&&"integrativeInput"===t.dimension&&r.some(e=>"methodology"===e.field&&!e.value.includes("integrative"))&&fail("Integrative input charts cannot exclude integrative structures.")}),"stacked"===i.stacking&&(!["bar","line","area","mixed"].includes(t.chartType)||t.series.length<2||"count"!==t.measure||"linear"!==t.scale||t.cumulative||!disjointCohorts(t))&&fail("Stacking requires count values, a linear scale, non-cumulative values and provably disjoint cohorts. Use grouped series for populations that may overlap."),t}function getChartPresentation(e){const t=validateChartSpec(e);return{...catalog.presentationDefaults,colors:[],seriesTypes:[],borderWidth:["line","area","radar","mixed"].includes(t.chartType)?2:1,...t.presentation}}function terminal(e){const t=catalog.filters[e.field];let r=e.value;return"year"===t.kind&&(r=`${r}-${["gte","lt"].includes(e.operator)?"01-01":"12-31"}`),"taxonomy"===t.kind&&(r=r.map(String)),{type:"terminal",service:"text",parameters:{attribute:t.attribute,operator:{in:"in",gte:"greater_or_equal",lte:"less_or_equal",gt:"greater",lt:"less"}[e.operator],value:r}}}function filterTerminals(e){const t=new Set;return e.flatMap(r=>{const a=catalog.filters[r.field];if(!["number","year"].includes(a.kind))return[terminal(r)];if(t.has(r.field))return[];t.add(r.field);const{from:i,to:n,include_lower:s,include_upper:o}=numericBounds(e,r.field);return Number.isFinite(i)&&Number.isFinite(n)?[{type:"terminal",service:"text",parameters:{attribute:a.attribute,operator:"range",value:{from:"year"===a.kind?`${i}-01-01`:i,to:"year"===a.kind?`${n}-12-31`:n,include_lower:s,include_upper:o}}}]:[terminal({...r,operator:Number.isFinite(i)?s?"gte":"gt":o?"lte":"lt",value:Number.isFinite(i)?i:n})]})}function buildObservationRequests(e){const t=validateChartSpec(e);return isObservationChart(t)||fail("Observation requests require a scatter or bubble chart."),t.series.map(e=>{const r=[...t.filters,...e.filters],a={return_type:"entry",request_options:{results_content_type:["experimental"],paginate:{start:0,rows:t.observations.limit},sort:[{sort_by:"rcsb_accession_info.initial_release_date",direction:"desc"},{sort_by:"rcsb_entry_container_identifiers.entry_id",direction:"asc"}]}};return r.length&&(a.query={type:"group",logical_operator:"and",nodes:filterTerminals(r).map(e=>({type:"group",logical_operator:"and",nodes:[e]}))}),a})}const validEntryId=e=>"string"==typeof e&&/^(?:[0-9][A-Z0-9]{3}|PDB_[A-Z0-9]{8})$/.test(e);function buildObservationDataRequest(e){return(!Array.isArray(e)||e.length>500||e.some(e=>!validEntryId(e))||new Set(e).size!==e.length)&&fail("Select at most 500 unique, valid PDB entry identifiers."),{query:"query ChartObservations($ids: [String!]!) { entries(entry_ids: $ids) { rcsb_id rcsb_accession_info { initial_release_date } rcsb_entry_info { resolution_combined molecular_weight deposited_atom_count deposited_polymer_monomer_count deposited_polymer_entity_instance_count polymer_entity_count } } }",variables:{ids:[...e]}}}function observationValue(e,t){const r=catalog.metrics[t].dataField.split(".").reduce((e,t)=>isObject(e)?e[t]:void 0,e);if("releaseYear"===t){if("string"!=typeof r||!/^\d{4}-\d{2}-\d{2}(?:T|$)/.test(r)||!Number.isFinite(Date.parse(r)))return null;if(new Date(r.slice(0,10)).toISOString().slice(0,10)!==r.slice(0,10))return null;const e=new Date(r).getUTCFullYear();return e>=1971&&e<=2100?e:null}if("resolution"===t){if(!Array.isArray(r)||r.some(e=>null!==e&&("number"!=typeof e||!Number.isFinite(e))))return null;const e=r.filter(e=>e>0);return e.length?Math.min(...e):null}return"number"!=typeof r||!Number.isFinite(r)||r<0||r>Number.MAX_SAFE_INTEGER?null:"molecularWeight"===t||Number.isSafeInteger(r)?r:null}function formatObservationData(e,t,r){const a=validateChartSpec(e);isObservationChart(a)||fail("Observation data requires a scatter or bubble chart."),(!Array.isArray(t)||t.length!==a.series.length||!Array.isArray(r)||r.length>500)&&fail("A bounded entry response is required for every observation cohort.");const i=new Set,n=t.map(e=>{(!isObject(e)||!Number.isSafeInteger(e.total_count)||e.total_count<0)&&fail("The Search API did not return a valid observation count.");const t=void 0===e.result_set&&0===e.total_count?[]:e.result_set;(!Array.isArray(t)||t.length>Math.min(e.total_count,a.observations.limit))&&fail("The Search API did not return a bounded observation selection.");const r=t.map(e=>(isObject(e)&&validEntryId(e.identifier)||fail("The Search API returned an invalid PDB entry identifier."),i.add(e.identifier),e.identifier));return new Set(r).size!==r.length&&fail("The Search API returned duplicate observation identifiers within a cohort."),r}),s=new Map;for(const e of r)null!==e&&(isObject(e)&&validEntryId(e.rcsb_id)&&i.has(e.rcsb_id)||fail("The Data API returned an entry outside the validated observation selection."),s.has(e.rcsb_id)&&fail("The Data API returned duplicate observation records."),(null!=e.rcsb_entry_info&&!isObject(e.rcsb_entry_info)||null!=e.rcsb_accession_info&&!isObject(e.rcsb_accession_info))&&fail("The Data API returned malformed entry measurements."),s.set(e.rcsb_id,e));const o=[],l=a.series.map((e,r)=>{const i=o.length;for(const e of n[r]){const t=s.get(e);if(!t)continue;const i=observationValue(t,a.observations.x),n=observationValue(t,a.observations.y),l=a.observations.size?observationValue(t,a.observations.size):void 0;null!==i&&null!==n&&null!==l&&o.push({entryId:e,seriesIndex:r,x:i,y:n,...void 0===l?{}:{size:l}})}return{label:e.label,total:t[r].total_count,selected:n[r].length,omitted:n[r].length-(o.length-i)}}),u=chartWarnings(a);for(const e of l)e.total>e.selected&&u.push(`${e.label}: selected ${e.selected} of ${e.total} matching entries; values do not describe the full matching population.`),e.omitted&&u.push(`${e.label}: omitted ${e.omitted} selected entries with missing, unavailable or invalid measurements; no values were imputed.`);return{kind:"records",rows:[],points:o,selectedEntryIds:n,series:l,warnings:u,retrievedAt:(new Date).toISOString(),categoryCount:0}}function buildChartRequests(e){const t=validateChartSpec(e);if(isObservationChart(t))return buildObservationRequests(t);const r=catalog.dimensions[t.dimension],a={name:"chart",aggregation_type:r.aggregation,attribute:r.attribute,min_interval_population:1};return"terms"===r.aggregation&&(a.max_num_intervals=1e3),"date_histogram"===r.aggregation&&(a.interval="year"),"histogram"===r.aggregation&&(a.interval=t.interval),t.series.map(e=>{const i=[...t.filters,...e.filters,...r.requiredFilter?[r.requiredFilter]:[]],n={return_type:"entry",request_options:{results_content_type:["experimental"],paginate:{start:0,rows:0},facets:[a]}};return i.length&&(n.query={type:"group",logical_operator:"and",nodes:filterTerminals(i).map(e=>({type:"group",logical_operator:"and",nodes:[e]}))}),n})}function buildChartDrilldownRequest(e,t,r){const a=validateChartSpec(e);isObservationChart(a)&&fail("Inspect observation points by their PDB entry identifier, not an aggregation bucket."),(!Number.isInteger(r)||r<0||r>=a.series.length)&&fail("Select a valid chart series."),("string"!=typeof t||!t.trim()||t.length>300||/[\u0000-\u001f\u007f]/.test(t))&&fail("Select a valid chart category.");const i=catalog.dimensions[a.dimension],n={attribute:i.attribute,operator:"exact_match",value:t};if("releaseYear"===a.dimension){(!/^\d{4}$/.test(t)||Number(t)<catalog.filters.releaseYear.minimum||Number(t)>catalog.filters.releaseYear.maximum)&&fail("Select a valid release year.");const e=`${Number(t)+1}-01-01`;n.operator=a.cumulative?"less":"range",n.value=a.cumulative?e:{from:`${t}-01-01`,to:e,include_lower:!0,include_upper:!1}}else if("histogram"===i.aggregation){const e=Number(t);(!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(t)||!Number.isFinite(e)||e<0||e>catalog.filters[a.dimension].maximum||Math.abs(e/a.interval-Math.round(e/a.interval))>1e-8)&&fail("Select a valid numeric interval."),n.operator="range",n.value={from:e,to:Number((e+a.interval).toPrecision(12)),include_lower:!0,include_upper:!1}}const s=buildChartRequests(a)[r];return{return_type:"entry",query:{type:"group",logical_operator:"and",nodes:[...s.query?.nodes||[],{type:"group",logical_operator:"and",nodes:[{type:"terminal",service:"text",parameters:n}]}]},request_options:{results_content_type:["experimental"],paginate:{start:0,rows:25}}}}function describeFilter(e){return`${catalog.filters[e.field].label} ${{in:"is any of",gte:"≥",lte:"≤",gt:">",lt:"<"}[e.operator]} ${Array.isArray(e.value)?e.value.join(", "):e.value}`}function describeChart(e){return validateChartSpec(e),isObservationChart(e)?`${chartMetricLabel(e.observations.y)} versus ${chartMetricLabel(e.observations.x)}${e.observations.size?`; bubble area: ${chartMetricLabel(e.observations.size)}`:""}; ${e.chartType} chart; up to ${e.observations.limit} newest entries per cohort.`:`${e.cumulative?"Cumulative ":""}${"count"===e.measure?"PDB entry counts":"Percentage of matching PDB entries"} by ${catalog.dimensions[e.dimension].label.toLowerCase()}; ${e.chartType} chart; ${e.series.length} ${1===e.series.length?"series":"comparison series"}.`}function chartWarnings(e){const t=[],r=isObservationChart(e),a={...catalog.presentationDefaults,...e.presentation};return r?(t.push("Points pair recorded measurements from the same PDB entry. Entry-level population filters may match different entities or reported values in that entry."),t.push(`Each cohort selects up to ${e.observations.limit} newest matching entries, ordered by initial release date descending and entry ID ascending. This is not a random or representative sample.`),Object.values(e.observations).includes("resolution")&&t.push("Resolution is the minimum positive reported combined resolution per entry, not an average or necessarily the value satisfying a resolution population filter. Entries without the required measurements are omitted, never assigned zero."),e.observations.size&&t.push("Bubble area is proportional to the recorded size metric. Its raw value and units are retained in the table and exports.")):catalog.dimensions[e.dimension].overlapping&&t.push("An entry may occur in more than one category. Bucket counts must not be summed as a unique entry total."),e.series.length>1&&"stacked"!==a.stacking&&t.push("Comparison cohorts may overlap; each is counted independently."),r||"sourceOrganism"!==e.dimension||t.push("Source organisms are those recorded for any matching entry; they are not expression hosts. All source types are included."),r||"releaseYear"!==e.dimension||t.push("Counts describe the current archive by initial release year, not historical archive snapshots. The current year is incomplete."),e.cumulative&&t.push("Cumulative values begin at the earliest year included by the filters."),"area"===e.chartType&&"stacked"!==a.stacking&&t.push("Area series use a zero baseline and are not stacked; overlapping cohorts are counted independently."),("logarithmic"===e.scale||r&&"logarithmic"===a.xScale)&&t.push("Zero values are omitted from the logarithmic chart; the table retains them."),"percent"===e.measure&&t.push("The denominator is every matching entry in that series, including entries missing the chart attribute."),"stacked"===a.stacking&&t.push("Stacked cohorts have disjoint entry populations. Stack totals describe only the included cohorts, not necessarily all matching archive entries."),["radar","polarArea"].includes(e.chartType)&&t.push("Radial values compare category magnitudes; they do not imply a complete, non-overlapping parts-of-whole partition."),["xMin","xMax","yMin","yMax"].some(e=>null!==a[e])&&t.push("Explicit axis bounds may hide values outside the visible chart. The table and exports retain all retrieved data."),t}function chartChanges(e,t){return e?(validateChartSpec(e),validateChartSpec(t),[...new Set([...Object.keys(e),...Object.keys(t)])].filter(r=>JSON.stringify(e[r])!==JSON.stringify(t[r])).flatMap(r=>{if("presentation"===r){const r=getChartPresentation(e),a=getChartPresentation(t),i={palette:"Color palette",legend:"Legend",grid:"Grid lines",lineStyle:"Line style",pointRadius:"Point radius",borderWidth:"Border width",barRadius:"Bar corner radius",doughnutCutout:"Doughnut cutout",colors:"Series colors",fillOpacity:"Fill opacity",pointStyle:"Point shape",lineDash:"Line pattern",barWidth:"Bar width",tickRotation:"Tick rotation",fontSize:"Font size",height:"Chart height",radialStartAngle:"Radial start angle",xScale:"X-axis scale",xMin:"X-axis minimum",xMax:"X-axis maximum",yMin:"Value-axis minimum",yMax:"Value-axis maximum",reverseX:"Reverse X axis",reverseY:"Reverse value axis",seriesTypes:"Series drawing types",stacking:"Stacking"},n=(e,t)=>"grid"===e?t?"shown":"hidden":"doughnutCutout"===e?`${t}%`:Array.isArray(t)?t.join(", ")||"automatic":null===t?"automatic":String(t);return Object.keys(a).filter(e=>JSON.stringify(r[e])!==JSON.stringify(a[e])).map(e=>`${i[e]}: ${n(e,r[e])} → ${n(e,a[e])}.`)}return"observations"===r?t.observations?`Measurements: ${chartMetricLabel(t.observations.x)}, ${chartMetricLabel(t.observations.y)}${t.observations.size?`; size: ${chartMetricLabel(t.observations.size)}`:""}; up to ${t.observations.limit} entries per cohort.`:"Use entry-count aggregations.":"filters"===r?`Filters: ${t.filters.length?t.filters.map(describeFilter).join("; "):"none"}.`:"series"===r?`Series: ${t.series.map(e=>`${e.label}${e.filters.length?` (${e.filters.map(describeFilter).join("; ")})`:""}`).join("; ")}.`:`${r}: ${String(e[r])} → ${String(t[r])}`})):["Create a new chart."]}function formatChartData(e,t){const r=validateChartSpec(e);isObservationChart(r)&&fail("Paired entry records are required for an observation chart."),Array.isArray(t)&&t.length===r.series.length||fail("A response is required for every chart series.");const a=catalog.dimensions[r.dimension];let i=!1;const n=t.map((e,t)=>{(!isObject(e)||!Number.isInteger(e.total_count)||e.total_count<0)&&fail("The Search API did not return a valid entry count.");const n=e.facets?.find(e=>"chart"===e.name)?.buckets;Array.isArray(n)||0===e.total_count||fail("The Search API did not return the requested chart aggregation."),(n||[]).length>=1e3&&(i=!0);const s=new Map;return(n||[]).forEach(t=>{(!Number.isInteger(t.population)||t.population<0||t.population>e.total_count||!["string","number"].includes(typeof t.label)||String(t.label).length>300)&&fail("The Search API returned an invalid chart bucket.");let i=String(t.label);if("releaseYear"===r.dimension){const e=i.match(/\d{4}/);e||fail("The Search API returned an invalid release year."),i=e[0]}"histogram"===a.aggregation&&((!i.trim()||!Number.isFinite(Number(i))||Number(i)<0||Number(i)>catalog.filters[r.dimension].maximum||a.integer&&!Number.isInteger(Number(i)))&&fail("The Search API returned an invalid numeric interval."),i=String(Number(Number(i).toPrecision(12))),Math.abs(Number(i)/r.interval-Math.round(Number(i)/r.interval))>1e-8&&fail("The Search API returned a numeric interval outside the requested bin grid.")),s.has(i)&&fail("The Search API returned duplicate chart buckets."),s.set(i,t.population)}),{label:r.series[t].label,total:e.total_count,values:s}});let s=[...new Set(n.flatMap(e=>[...e.values.keys()]))];const o=s.every(e=>Number.isFinite(Number(e)));if(s.sort((e,t)=>o?Number(e)-Number(t):e.localeCompare(t)),"releaseYear"===r.dimension&&s.length){const e=Number(s[0]),t=Number(s[s.length-1]);t-e>200&&fail("The Search API returned an unsupported year range."),s=Array.from({length:t-e+1},(t,r)=>String(e+r))}if("histogram"===a.aggregation&&s.length){const e=Number(s[0]),t=Math.round((Number(s[s.length-1])-e)/r.interval)+1;t>1e3&&fail("This histogram exceeds 1,000 bins. Increase the bin width or narrow the numeric range."),s=Array.from({length:t},(t,a)=>String(Number((e+a*r.interval).toPrecision(12))))}const l=s.map(e=>({label:e,counts:n.map(t=>t.values.get(e)||0)}));if(r.cumulative){const e=n.map(()=>0);l.forEach(t=>{t.counts=t.counts.map((t,r)=>e[r]+=t)})}"countDesc"===r.sort&&l.sort((e,t)=>t.counts.reduce((e,t)=>e+t,0)-e.counts.reduce((e,t)=>e+t,0)||e.label.localeCompare(t.label));const u=!a.ordered,c=(u?l.slice(0,r.topN):l).map(e=>({...e,values:e.counts.map((e,t)=>"percent"===r.measure?n[t].total?100*e/n[t].total:0:e)}));("radar"===r.chartType&&c.length>20||"polarArea"===r.chartType&&c.length>30)&&fail("This radial chart has too many categories. Narrow the population or choose a bar chart.");const d=chartWarnings(r);return u&&l.length>c.length&&d.push(`Showing ${c.length} of ${l.length}${i?"+":""} categories returned. Omitted categories are not combined into an invented “Other” count.`),i&&d.push("The category aggregation reached its 1,000-bucket limit. Narrow the filters for a more complete category listing."),["pie","doughnut"].includes(r.chartType)&&(i||c.length!==l.length)&&fail("A pie or doughnut chart must show all categories. Increase the category limit or use a bar chart."),["pie","doughnut"].includes(r.chartType)&&c.reduce((e,t)=>e+t.counts[0],0)!==n[0].total&&fail("A pie or doughnut chart requires complete, non-overlapping categories covering every matching entry. Use a bar chart for this population."),{rows:c,series:n.map(({label:e,total:t})=>({label:e,total:t})),warnings:d,retrievedAt:(new Date).toISOString(),categoryCount:l.length}}module.exports={catalog,validateSchema,validateChartSpec,getChartPresentation,buildChartRequests,buildChartDrilldownRequest,describeFilter,describeChart,chartWarnings,chartChanges,formatChartData,isObservationChart,chartMetricLabel,buildObservationRequests,buildObservationDataRequest,formatObservationData};
|
|
1
|
+
const catalog=require("./chart-catalog.json"),isObject=e=>Boolean(e&&"object"==typeof e&&!Array.isArray(e)),has=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),fail=e=>{throw new Error(e)};function validateSchema(e,t,r=t,a="Chart"){if(t.$ref)return validateSchema(e,r.$defs[t.$ref.split("/").pop()],r,a);if(t.anyOf)return void(t.anyOf.some(t=>{try{return validateSchema(e,t,r,a),!0}catch{return!1}})||fail(`${a} has an invalid value.`));const i=t.type;("object"===i&&!isObject(e)||"array"===i&&!Array.isArray(e)||"integer"===i&&!Number.isInteger(e)||"number"===i&&("number"!=typeof e||!Number.isFinite(e))||"string"===i&&"string"!=typeof e||"boolean"===i&&"boolean"!=typeof e||"null"===i&&null!==e)&&fail(`${a} must be ${i}.`),t.enum&&!t.enum.includes(e)&&fail(`${a} is not supported.`),"number"==typeof e&&(void 0!==t.minimum&&e<t.minimum||void 0!==t.maximum&&e>t.maximum)&&fail(`${a} is outside the supported range.`),"string"==typeof e&&(t.minLength&&e.trim().length<t.minLength||t.maxLength&&e.length>t.maxLength||/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(e))&&fail(`${a} has invalid text.`),"string"==typeof e&&t.pattern&&!new RegExp(t.pattern).test(e)&&fail(`${a} has invalid text.`),Array.isArray(e)&&((t.minItems&&e.length<t.minItems||t.maxItems&&e.length>t.maxItems||t.uniqueItems&&new Set(e.map(e=>JSON.stringify(e))).size!==e.length)&&fail(`${a} has an invalid number of items.`),e.forEach((e,i)=>validateSchema(e,t.items,r,`${a}[${i}]`))),isObject(e)&&((t.required||[]).forEach(t=>{has(e,t)||fail(`${a}.${t} is required.`)}),Object.keys(e).forEach(i=>{has(t.properties||{},i)?validateSchema(e[i],t.properties[i],r,`${a}.${i}`):!1===t.additionalProperties&&fail(`${a}.${i} is not supported.`)}))}const isObservationChart=e=>["scatter","bubble"].includes(e.chartType),chartMetricLabel=e=>{has(catalog.metrics,e)||fail("Select a supported observation metric.");const{label:t,unit:r}=catalog.metrics[e];return`${t} (${r})`};function numericBounds(e,t){let r=-1/0,a=1/0,i=!0,n=!0;return e.filter(e=>e.field===t).forEach(e=>{const s=["gte","gt"].includes(e.operator),o=["gt","lt"].includes(e.operator),l="releaseYear"===t||catalog.filters[t].integer,u=e.value+(l&&o?s?1:-1:0),c=l||!o;s&&(u>r||u===r&&!c)&&(r=u,i=c),!s&&(u<a||u===a&&!c)&&(a=u,n=c)}),{from:r,to:a,include_lower:i,include_upper:n}}function disjointCohorts(e){const t=e.series.map(t=>[...e.filters,...t.filters]),r=e=>e.filter(e=>"methodology"===e.field).reduce((e,t)=>e.filter(e=>t.value.includes(e)),catalog.filters.methodology.values),a=Object.keys(catalog.filters).filter(e=>["number","year"].includes(catalog.filters[e].kind)&&!1===catalog.dimensions[e]?.overlapping);return t.every((e,i)=>t.slice(i+1).every(t=>!r(e).some(e=>r(t).includes(e))||a.some(r=>{const a=numericBounds(e,r),i=numericBounds(t,r);return a.to<i.from||i.to<a.from||a.to===i.from&&(!a.include_upper||!i.include_lower)||i.to===a.from&&(!i.include_upper||!a.include_lower)})))}function validateChartSpec(e){isObject(e)&&["scatter","bubble"].includes(e.chartType)&&isObject(e.observations)&&["x","y","size"].some(t=>"releaseYear"===e.observations[t])&&fail("Release year cannot be an observation axis because selection takes newest entries. Use an aggregate release-year chart instead."),validateSchema(e,catalog.specSchema);const t=JSON.parse(JSON.stringify(e)),r=catalog.dimensions[t.dimension],a=isObservationChart(t),i={...catalog.presentationDefaults,...t.presentation};[t.title,t.categoryLabel,t.valueLabel,...t.series.map(e=>e.label)].some(e=>/\bcryo\b|\bcryo(?:genic)?(?:[\s\u2010-\u2015-]*(?:em|electron(?:[\s\u2010-\u2015-]+microscopy)?))\b/i.test(e))&&fail("Cryo-EM labels require a verified specimen-preparation constraint, which this chart catalog does not support. Use an accurately described experimental-method chart or clarify the population."),t.observations&&Object.values(t.observations).includes("releaseYear")&&fail("Release year cannot be an observation axis because selection takes newest entries. Use an aggregate release-year chart instead."),!["line","area","mixed"].includes(t.chartType)||r.ordered&&"vertical"===t.orientation&&"labelAsc"===t.sort||fail("Line, area and mixed charts require an ordered year or numeric dimension, vertical orientation, and ascending labels."),a||!r.integer||Number.isInteger(t.interval)||fail("Count histograms require a whole-number bin width of at least 1."),("area"===t.chartType||"mixed"===t.chartType&&i.seriesTypes.includes("area"))&&"linear"!==t.scale&&fail("Area charts require a linear scale for their zero baseline."),["pie","doughnut"].includes(t.chartType)&&(r.overlapping||1!==t.series.length||t.cumulative||"linear"!==t.scale)&&fail("Pie and doughnut charts require a single non-overlapping series, linear scale, and non-cumulative values."),["radar","polarArea"].includes(t.chartType)&&("linear"!==t.scale||t.cumulative||t.topN>("radar"===t.chartType?20:30))&&fail("Radar and polar-area charts require a linear scale, non-cumulative values and a bounded category limit (20 for radar, 30 for polar area)."),"polarArea"===t.chartType&&1!==t.series.length&&fail("Polar-area charts require one series. Use radar or grouped bars to compare cohorts."),"mixed"===t.chartType?(i.seriesTypes.length!==t.series.length||new Set(i.seriesTypes).size<2)&&fail("Mixed charts require a drawing type for every series and at least two different types."):i.seriesTypes.length&&fail("Per-series drawing types are only supported for mixed charts."),a?(t.observations||fail("Scatter and bubble charts require paired entry observations."),t.observations.limit*t.series.length>500&&fail("Select at most 500 entry observations across all cohorts."),("count"!==t.measure||t.cumulative||"vertical"!==t.orientation)&&fail("Observation charts require count mode, vertical orientation and non-cumulative values."),("bubble"===t.chartType&&!t.observations.size||"scatter"===t.chartType&&null!==t.observations.size)&&fail("Bubble charts require a size metric; scatter charts require a null size metric.")):null!=t.observations&&fail("Entry observations are only supported for scatter and bubble charts."),a||"linear"===i.xScale&&null===i.xMin&&null===i.xMax||fail("Numeric X-axis scales and bounds are only supported for observation charts.");for(const e of["x","y"]){const r=i[`${e}Min`],a=i[`${e}Max`];null!==r&&null!==a&&r>=a&&fail("An axis minimum must be less than its maximum."),"logarithmic"===("x"===e?i.xScale:t.scale)&&[r,a].some(e=>null!==e&&e<=0)&&fail("Logarithmic axis bounds must be positive.")}return!t.cumulative||"releaseYear"===t.dimension&&"labelAsc"===t.sort||fail("Cumulative charts require release year in chronological order."),"archive"!==t.cumulativeBase||t.cumulative&&"releaseYear"===t.dimension&&"count"===t.measure||fail("Archive cumulative mode requires cumulative release-year counts. Percentages need an explicit denominator and are not supported in this mode."),"percent"===t.measure&&"linear"!==t.scale&&fail("Percentages require a linear scale."),new Set(t.series.map(e=>e.label)).size!==t.series.length&&fail("Give each comparison series a different label."),[t.filters,...t.series.map(e=>e.filters)].forEach(e=>e.forEach(e=>{const t=catalog.filters[e.field];t.operators.includes(e.operator)||fail(`Unsupported operator for ${t.label}.`),"in"===e.operator?(Array.isArray(e.value)||fail(`${t.label} requires a list.`),e.value.forEach(e=>{"taxonomy"===t.kind?(!Number.isInteger(e)||e<=0)&&fail("Taxonomy IDs must be positive integers."):("string"!=typeof e||t.values&&!t.values.includes(e)||"identifier"===t.kind&&!/^[A-Z0-9]{1,10}$/.test(e))&&fail(`Invalid value for ${t.label}.`)})):("number"!=typeof e.value||e.value<t.minimum||e.value>t.maximum||("year"===t.kind||t.integer)&&!Number.isInteger(e.value))&&fail(`Invalid range for ${t.label}.`)})),t.series.forEach(e=>{const r=[...t.filters,...e.filters];"archive"!==t.cumulativeBase||Number.isFinite(numericBounds(r,"releaseYear").from)||fail("Archive cumulative mode requires an explicit lower release year for each cohort."),Object.keys(catalog.filters).filter(e=>["number","year"].includes(catalog.filters[e].kind)).forEach(e=>{const t=numericBounds(r,e),a=catalog.filters[e].integer,i=t.from,n=t.to;!(i>n)&&(i!==n||a||t.include_lower&&t.include_upper)||fail(`The ${e} lower bound exceeds its upper bound or defines an empty interval.`)}),!a&&"integrativeInput"===t.dimension&&r.some(e=>"methodology"===e.field&&!e.value.includes("integrative"))&&fail("Integrative input charts cannot exclude integrative structures.")}),"archive"===t.cumulativeBase&&1!==new Set(t.series.map(e=>numericBounds([...t.filters,...e.filters],"releaseYear").from)).size&&fail("Archive cumulative comparison cohorts must use the same lower release year. Choose a shared display start or use window cumulative counts."),"stacked"===i.stacking&&(!["bar","line","area","mixed"].includes(t.chartType)||t.series.length<2||"count"!==t.measure||"linear"!==t.scale||t.cumulative||!disjointCohorts(t))&&fail("Stacking requires count values, a linear scale, non-cumulative values and provably disjoint cohorts. Use grouped series for populations that may overlap."),t}function getChartPresentation(e){const t=validateChartSpec(e);return{...catalog.presentationDefaults,colors:[],seriesTypes:[],borderWidth:["line","area","radar","mixed"].includes(t.chartType)?2:1,...t.presentation}}function terminal(e){const t=catalog.filters[e.field];let r=e.value;return"year"===t.kind&&(r=`${r}-${["gte","lt"].includes(e.operator)?"01-01":"12-31"}`),"taxonomy"===t.kind&&(r=r.map(String)),{type:"terminal",service:"text",parameters:{attribute:t.attribute,operator:{in:"in",gte:"greater_or_equal",lte:"less_or_equal",gt:"greater",lt:"less"}[e.operator],value:r}}}function filterTerminals(e){const t=new Set;return e.flatMap(r=>{const a=catalog.filters[r.field];if(!["number","year"].includes(a.kind))return[terminal(r)];if(t.has(r.field))return[];t.add(r.field);const{from:i,to:n,include_lower:s,include_upper:o}=numericBounds(e,r.field);return Number.isFinite(i)&&Number.isFinite(n)?"resolution"===r.field||"year"===a.kind||a.integer?[{type:"terminal",service:"text",parameters:{attribute:a.attribute,operator:"range",value:{from:"year"===a.kind?`${i}-01-01`:i,to:"year"===a.kind?`${n}-12-31`:n,include_lower:s,include_upper:o}}}]:[terminal({field:r.field,operator:s?"gte":"gt",value:i}),terminal({field:r.field,operator:o?"lte":"lt",value:n})]:[terminal({...r,operator:Number.isFinite(i)?s?"gte":"gt":o?"lte":"lt",value:Number.isFinite(i)?i:n})]})}function buildObservationRequests(e){const t=validateChartSpec(e);return isObservationChart(t)||fail("Observation requests require a scatter or bubble chart."),t.series.map(e=>{const r=[...t.filters,...e.filters],a={return_type:"entry",request_options:{results_content_type:["experimental"],paginate:{start:0,rows:t.observations.limit},sort:[{sort_by:"rcsb_accession_info.initial_release_date",direction:"desc"},{sort_by:"rcsb_entry_container_identifiers.entry_id",direction:"asc"}]}};return r.length&&(a.query={type:"group",logical_operator:"and",nodes:filterTerminals(r).map(e=>({type:"group",logical_operator:"and",nodes:[e]}))}),a})}const validEntryId=e=>"string"==typeof e&&/^(?:[0-9][A-Z0-9]{3}|PDB_[A-Z0-9]{8})$/.test(e);function buildObservationDataRequest(e){return(!Array.isArray(e)||e.length>500||e.some(e=>!validEntryId(e))||new Set(e).size!==e.length)&&fail("Select at most 500 unique, valid PDB entry identifiers."),{query:"query ChartObservations($ids: [String!]!) { entries(entry_ids: $ids) { rcsb_id rcsb_accession_info { initial_release_date } rcsb_entry_info { resolution_combined molecular_weight deposited_atom_count deposited_polymer_monomer_count deposited_polymer_entity_instance_count polymer_entity_count } } }",variables:{ids:[...e]}}}function observationValue(e,t){const r=catalog.metrics[t].dataField.split(".").reduce((e,t)=>isObject(e)?e[t]:void 0,e);if("releaseYear"===t){if("string"!=typeof r||!/^\d{4}-\d{2}-\d{2}(?:T|$)/.test(r)||!Number.isFinite(Date.parse(r)))return null;if(new Date(r.slice(0,10)).toISOString().slice(0,10)!==r.slice(0,10))return null;const e=new Date(r).getUTCFullYear();return e>=1971&&e<=2100?e:null}if("resolution"===t){if(!Array.isArray(r)||r.some(e=>null!==e&&("number"!=typeof e||!Number.isFinite(e))))return null;const e=r.filter(e=>e>0);return e.length?Math.min(...e):null}return"number"!=typeof r||!Number.isFinite(r)||r<0||r>Number.MAX_SAFE_INTEGER?null:"molecularWeight"===t||Number.isSafeInteger(r)?r:null}function formatObservationData(e,t,r){const a=validateChartSpec(e);isObservationChart(a)||fail("Observation data requires a scatter or bubble chart."),(!Array.isArray(t)||t.length!==a.series.length||!Array.isArray(r)||r.length>500)&&fail("A bounded entry response is required for every observation cohort.");const i=new Set,n=t.map(e=>{(!isObject(e)||!Number.isSafeInteger(e.total_count)||e.total_count<0)&&fail("The Search API did not return a valid observation count.");const t=void 0===e.result_set&&0===e.total_count?[]:e.result_set;(!Array.isArray(t)||t.length>Math.min(e.total_count,a.observations.limit))&&fail("The Search API did not return a bounded observation selection.");const r=t.map(e=>(isObject(e)&&validEntryId(e.identifier)||fail("The Search API returned an invalid PDB entry identifier."),i.add(e.identifier),e.identifier));return new Set(r).size!==r.length&&fail("The Search API returned duplicate observation identifiers within a cohort."),r}),s=new Map;for(const e of r)null!==e&&(isObject(e)&&validEntryId(e.rcsb_id)&&i.has(e.rcsb_id)||fail("The Data API returned an entry outside the validated observation selection."),s.has(e.rcsb_id)&&fail("The Data API returned duplicate observation records."),(null!=e.rcsb_entry_info&&!isObject(e.rcsb_entry_info)||null!=e.rcsb_accession_info&&!isObject(e.rcsb_accession_info))&&fail("The Data API returned malformed entry measurements."),s.set(e.rcsb_id,e));const o=[],l=a.series.map((e,r)=>{const i=o.length;for(const e of n[r]){const t=s.get(e);if(!t)continue;const i=observationValue(t,a.observations.x),n=observationValue(t,a.observations.y),l=a.observations.size?observationValue(t,a.observations.size):void 0;null!==i&&null!==n&&null!==l&&o.push({entryId:e,seriesIndex:r,x:i,y:n,...void 0===l?{}:{size:l}})}return{label:e.label,total:t[r].total_count,selected:n[r].length,omitted:n[r].length-(o.length-i)}}),u=chartWarnings(a);for(const e of l)e.total>e.selected&&u.push(`${e.label}: selected ${e.selected} of ${e.total} matching entries; values do not describe the full matching population.`),e.omitted&&u.push(`${e.label}: omitted ${e.omitted} selected entries with missing, unavailable or invalid measurements; no values were imputed.`);return{kind:"records",rows:[],points:o,selectedEntryIds:n,series:l,warnings:u,retrievedAt:(new Date).toISOString(),categoryCount:0}}function buildChartRequests(e){const t=validateChartSpec(e);if(isObservationChart(t))return buildObservationRequests(t);const r=catalog.dimensions[t.dimension],a={name:"chart",aggregation_type:r.aggregation,attribute:r.attribute,min_interval_population:1};return"terms"===r.aggregation&&(a.max_num_intervals=1e3),"date_histogram"===r.aggregation&&(a.interval="year"),"histogram"===r.aggregation&&(a.interval=t.interval),t.series.map(e=>{const i=[...t.filters,...e.filters,...r.requiredFilter?[r.requiredFilter]:[]],n={return_type:"entry",request_options:{results_content_type:["experimental"],paginate:{start:0,rows:0},facets:[a]}};return i.length&&(n.query={type:"group",logical_operator:"and",nodes:filterTerminals(i).map(e=>({type:"group",logical_operator:"and",nodes:[e]}))}),n})}function buildChartBaselineRequests(e){const t=validateChartSpec(e);return"archive"!==t.cumulativeBase?[]:t.series.map(e=>{const r=[...t.filters,...e.filters],a=numericBounds(r,"releaseYear").from,i=filterTerminals(r.filter(e=>"releaseYear"!==e.field));return i.push({type:"terminal",service:"text",parameters:{attribute:catalog.filters.releaseYear.attribute,operator:"less",value:`${a}-01-01`}}),{return_type:"entry",query:{type:"group",logical_operator:"and",nodes:i.map(e=>({type:"group",logical_operator:"and",nodes:[e]}))},request_options:{results_content_type:["experimental"],paginate:{start:0,rows:0}}}})}function estimateChartRequests(e){const t=validateChartSpec(e),r=isObservationChart(t),a=r?0:t.series.length,i="archive"===t.cumulativeBase?t.series.length:0,n=r?t.series.length:0,s=r?1:0;return{aggregateRequests:a,baselineRequests:i,selectionRequests:n,dataRequests:s,totalRequests:a+i+n+s}}function chartBinBounds(e,t){const r=validateChartSpec(e);if("histogram"!==catalog.dimensions[r.dimension].aggregation)return null;const a=Number(t);return Number.isFinite(a)||fail("Select a valid numeric interval."),{lower:a,upperExclusive:Number((a+r.interval).toPrecision(12))}}function formatChartBucketLabel(e,t,r=!1){const a=catalog.dimensions[e.dimension];if("histogram"!==a.aggregation)return`${t}${r?" (to date)":""}`;const i=Number(t),n=Number((i+e.interval).toPrecision(12)),s=a.unit||"";if(!a.integer)return`${t} to <${n}${s?` ${s}`:""}`;const o={atomCount:["atom","atoms"],monomerCount:["monomer","monomers"],chainCount:["chain","chains"],polymerEntityCount:["entity","entities"]}[e.dimension];return 1===e.interval?`${t} ${1===i?o[0]:o[1]}`:`${t}–${n-1} ${o[1]}`}function buildChartDrilldownRequest(e,t,r){const a=validateChartSpec(e);isObservationChart(a)&&fail("Inspect observation points by their PDB entry identifier, not an aggregation bucket."),(!Number.isInteger(r)||r<0||r>=a.series.length)&&fail("Select a valid chart series."),("string"!=typeof t||!t.trim()||t.length>300||/[\u0000-\u001f\u007f]/.test(t))&&fail("Select a valid chart category.");const i=catalog.dimensions[a.dimension],n={attribute:i.attribute,operator:"exact_match",value:t};if("releaseYear"===a.dimension){(!/^\d{4}$/.test(t)||Number(t)<catalog.filters.releaseYear.minimum||Number(t)>catalog.filters.releaseYear.maximum)&&fail("Select a valid release year.");const e=`${Number(t)+1}-01-01`;n.operator=a.cumulative?"less":"range",n.value=a.cumulative?e:{from:`${t}-01-01`,to:e,include_lower:!0,include_upper:!1}}else if("histogram"===i.aggregation){const e=Number(t);(!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(t)||!Number.isFinite(e)||e<0||e>catalog.filters[a.dimension].maximum||Math.abs(e/a.interval-Math.round(e/a.interval))>1e-8)&&fail("Select a valid numeric interval."),n.operator="range",n.value={from:e,to:Number((e+a.interval).toPrecision(12)),include_lower:!0,include_upper:!1}}const s=e=>"releaseYear"!==e.field||["lt","lte"].includes(e.operator),o="archive"===a.cumulativeBase?buildChartRequests({...a,cumulativeBase:"window",filters:a.filters.filter(s),series:a.series.map(e=>({...e,filters:e.filters.filter(s)}))})[r]:buildChartRequests(a)[r];return{return_type:"entry",query:{type:"group",logical_operator:"and",nodes:[...o.query?.nodes||[],{type:"group",logical_operator:"and",nodes:[{type:"terminal",service:"text",parameters:n}]}]},request_options:{results_content_type:["experimental"],paginate:{start:0,rows:25}}}}function describeFilter(e){return`${catalog.filters[e.field].label} ${{in:"is any of",gte:"≥",lte:"≤",gt:">",lt:"<"}[e.operator]} ${Array.isArray(e.value)?e.value.join(", "):e.value}`}function describeChart(e){return validateChartSpec(e),isObservationChart(e)?`${chartMetricLabel(e.observations.y)} versus ${chartMetricLabel(e.observations.x)}${e.observations.size?`; bubble area: ${chartMetricLabel(e.observations.size)}`:""}; ${e.chartType} chart; up to ${e.observations.limit} newest entries per cohort.`:`${e.cumulative?("archive"===e.cumulativeBase?"Archive":"Window")+" cumulative ":""}${"count"===e.measure?"PDB entry counts":"Percentage of matching PDB entries"} by ${catalog.dimensions[e.dimension].label.toLowerCase()}; ${e.chartType} chart; ${e.series.length} ${1===e.series.length?"series":"comparison series"}.`}function chartWarnings(e){const t=[],r=isObservationChart(e),a={...catalog.presentationDefaults,...e.presentation};return r?(t.push("Points pair recorded measurements from the same PDB entry. Entry-level population filters may match different entities or reported values in that entry."),t.push(`Each cohort selects up to ${e.observations.limit} newest matching entries, ordered by initial release date descending and entry ID ascending. This is not a random or representative sample.`),Object.values(e.observations).includes("resolution")&&t.push("Resolution is the minimum positive reported combined resolution per entry, not an average or necessarily the value satisfying a resolution population filter. Entries without the required measurements are omitted, never assigned zero."),e.observations.size&&t.push("Bubble area is proportional to the recorded size metric. Its raw value and units are retained in the table and exports.")):catalog.dimensions[e.dimension].overlapping&&t.push("An entry may occur in more than one category. Bucket counts must not be summed as a unique entry total."),e.series.length>1&&"stacked"!==a.stacking&&t.push("Comparison cohorts may overlap; each is counted independently."),r||"sourceOrganism"!==e.dimension||t.push("Source organisms are those recorded for any matching entry; they are not expression hosts. All source types are included."),r||"releaseYear"!==e.dimension||t.push("Counts describe the current archive by initial release year, not historical archive snapshots. The current year is incomplete."),e.cumulative&&t.push("archive"===e.cumulativeBase?"Archive cumulative values include a separately retrieved count before the displayed release-year window, with all other cohort filters retained. These are current-archive counts by initial release year, not historical archive snapshots.":"Window cumulative values begin at the earliest year included by the filters."),"area"===e.chartType&&"stacked"!==a.stacking&&t.push("Area series use a zero baseline and are not stacked; overlapping cohorts are counted independently."),("logarithmic"===e.scale||r&&"logarithmic"===a.xScale)&&t.push("Zero values are omitted from the logarithmic chart; the table retains them."),"percent"===e.measure&&t.push("The denominator is every matching entry in that series, including entries missing the chart attribute."),"stacked"===a.stacking&&t.push("Stacked cohorts have disjoint entry populations. Stack totals describe only the included cohorts, not necessarily all matching archive entries."),["radar","polarArea"].includes(e.chartType)&&t.push("Radial values compare category magnitudes; they do not imply a complete, non-overlapping parts-of-whole partition."),["xMin","xMax","yMin","yMax"].some(e=>null!==a[e])&&t.push("Explicit axis bounds may hide values outside the visible chart. The table and exports retain all retrieved data."),t}function chartChanges(e,t){return e?(validateChartSpec(e),validateChartSpec(t),[...new Set([...Object.keys(e),...Object.keys(t)])].filter(r=>JSON.stringify(e[r])!==JSON.stringify(t[r])).flatMap(r=>{if("presentation"===r){const r=getChartPresentation(e),a=getChartPresentation(t),i={palette:"Color palette",legend:"Legend",grid:"Grid lines",lineStyle:"Line style",pointRadius:"Point radius",borderWidth:"Border width",barRadius:"Bar corner radius",doughnutCutout:"Doughnut cutout",colors:"Series colors",fillOpacity:"Fill opacity",pointStyle:"Point shape",lineDash:"Line pattern",barWidth:"Bar width",tickRotation:"Tick rotation",fontSize:"Font size",height:"Chart height",radialStartAngle:"Radial start angle",xScale:"X-axis scale",xMin:"X-axis minimum",xMax:"X-axis maximum",yMin:"Value-axis minimum",yMax:"Value-axis maximum",reverseX:"Reverse X axis",reverseY:"Reverse value axis",seriesTypes:"Series drawing types",stacking:"Stacking"},n=(e,t)=>"grid"===e?t?"shown":"hidden":"doughnutCutout"===e?`${t}%`:Array.isArray(t)?t.join(", ")||"automatic":null===t?"automatic":String(t);return Object.keys(a).filter(e=>JSON.stringify(r[e])!==JSON.stringify(a[e])).map(e=>`${i[e]}: ${n(e,r[e])} → ${n(e,a[e])}.`)}return"observations"===r?t.observations?`Measurements: ${chartMetricLabel(t.observations.x)}, ${chartMetricLabel(t.observations.y)}${t.observations.size?`; size: ${chartMetricLabel(t.observations.size)}`:""}; up to ${t.observations.limit} entries per cohort.`:"Use entry-count aggregations.":"filters"===r?`Filters: ${t.filters.length?t.filters.map(describeFilter).join("; "):"none"}.`:"series"===r?`Series: ${t.series.map(e=>`${e.label}${e.filters.length?` (${e.filters.map(describeFilter).join("; ")})`:""}`).join("; ")}.`:`${r}: ${String(e[r])} → ${String(t[r])}`})):["Create a new chart."]}function formatChartData(e,t,r=[],a={}){const i=validateChartSpec(e);isObservationChart(i)&&fail("Paired entry records are required for an observation chart."),Array.isArray(t)&&t.length===i.series.length||fail("A response is required for every chart series.");const n=catalog.dimensions[i.dimension],s=void 0===a.retrievedAt?(new Date).toISOString():a.retrievedAt;"string"==typeof s&&Number.isFinite(Date.parse(s))||fail("A valid chart retrieval time is required.");const o=new Date(s).getUTCFullYear();let l=!1;Array.isArray(r)&&r.length===("archive"===i.cumulativeBase?i.series.length:0)||fail("A baseline count is required for every archive cumulative series, and only for archive mode.");const u=r.map(e=>((!isObject(e)||!Number.isSafeInteger(e.total_count)||e.total_count<0)&&fail("The Search API did not return a valid archive baseline count."),e.total_count));let c=!1;const d=t.map((e,t)=>{(!isObject(e)||!Number.isSafeInteger(e.total_count)||e.total_count<0)&&fail("The Search API did not return a valid entry count.");const r=e.facets?.find(e=>"chart"===e.name)?.buckets;Array.isArray(r)||0===e.total_count||fail("The Search API did not return the requested chart aggregation."),(r||[]).length>=1e3&&(c=!0);const a=new Map;return(r||[]).forEach(r=>{(!isObject(r)||!Number.isSafeInteger(r.population)||r.population<0||r.population>e.total_count||!["string","number"].includes(typeof r.label)||!String(r.label).trim()||String(r.label).length>300||/[\u0000-\u001f\u007f]/.test(String(r.label)))&&fail("The Search API returned an invalid chart bucket.");let s=String(r.label);if("releaseYear"===i.dimension){const e=s.match(/^(\d{4})(?:-01-01(?:T00:00:00(?:\.0{1,3})?(?:Z|\+00:00))?)?$/);(!e||Number(e[1])<catalog.filters.releaseYear.minimum||Number(e[1])>catalog.filters.releaseYear.maximum)&&fail("The Search API returned an invalid release year."),s=e[1];const r=numericBounds([...i.filters,...i.series[t].filters],"releaseYear");(Number(s)<r.from||Number(s)>r.to||Number(s)>o)&&fail("The Search API returned a release year outside the requested cohort or retrieval date.")}"histogram"===n.aggregation&&((!s.trim()||!Number.isFinite(Number(s))||Number(s)<0||Number(s)>catalog.filters[i.dimension].maximum||n.integer&&!Number.isInteger(Number(s)))&&fail("The Search API returned an invalid numeric interval."),s=String(Number(Number(s).toPrecision(12))),Math.abs(Number(s)/i.interval-Math.round(Number(s)/i.interval))>1e-8&&fail("The Search API returned a numeric interval outside the requested bin grid.")),a.has(s)&&fail("The Search API returned duplicate chart buckets."),a.set(s,r.population)}),!n.overlapping&&[...a.values()].reduce((e,t)=>e+t,0)>e.total_count&&fail("The Search API returned non-overlapping bucket counts exceeding the matching entry total."),Number.isSafeInteger(e.total_count+(u[t]||0))||fail("The cumulative entry count exceeds the supported safe integer range."),{label:i.series[t].label,total:e.total_count+(u[t]||0),values:a,..."archive"===i.cumulativeBase?{windowTotal:e.total_count,baselineCount:u[t]}:{}}});let m=[...new Set(d.flatMap(e=>[...e.values.keys()]))];const h=m.every(e=>Number.isFinite(Number(e)));if(m.sort((e,t)=>h?Number(e)-Number(t):e.localeCompare(t)),"releaseYear"===i.dimension){const e=i.series.map((e,t)=>{const r=numericBounds([...i.filters,...e.filters],i.dimension),a=[...d[t].values.keys()].map(Number);return{first:Number.isFinite(r.from)?r.from:a.length?Math.min(...a):1/0,last:Number.isFinite(r.to)?Math.min(r.to,o):a.length?Math.max(...a):-1/0}}).filter(e=>e.first<=e.last),t=Math.min(...e.map(e=>e.first)),r=Math.max(...e.map(e=>e.last));r-t>200&&fail("The Search API returned an unsupported year range."),m=e.length?Array.from({length:r-t+1},(e,r)=>String(t+r)):[]}if("histogram"===n.aggregation){const e=e=>Number((e/i.interval).toPrecision(12)),t=i.series.map((t,r)=>{const a=numericBounds([...i.filters,...t.filters],i.dimension),n=[...d[r].values.keys()].map(t=>Math.round(e(Number(t)))),s=Number.isFinite(a.from)?Math.floor(e(a.from)):n.length?Math.min(...n):1/0,o=Number.isFinite(a.to)?a.include_upper?Math.floor(e(a.to)):Math.ceil(e(a.to))-1:n.length?Math.max(...n):-1/0;return{first:Math.min(s,...n),last:Math.max(o,...n)}}).filter(e=>e.first<=e.last),r=Math.min(...t.map(e=>e.first)),a=Math.max(...t.map(e=>e.last)),n=t.length?a-r+1:0;if(n>1e3){const t=m.length?Math.round(e(Number(m[0]))):0,r=(m.length?Math.round(e(Number(m[m.length-1]))):-1)-t+1;r>1e3&&fail("This histogram exceeds 1,000 bins. Increase the bin width or narrow the numeric range."),m=Array.from({length:r},(e,r)=>String(Number(((t+r)*i.interval).toPrecision(12)))),l=!0}else m=Array.from({length:n},(e,t)=>String(Number(((r+t)*i.interval).toPrecision(12))))}const p=m.map(e=>({label:e,counts:d.map(t=>t.values.get(e)||0)}));if(i.cumulative){const e=d.map((e,t)=>u[t]||0);p.forEach(t=>{t.counts=t.counts.map((t,r)=>e[r]+=t)})}const g=!n.ordered,b=(e,t)=>t.counts.reduce((e,t)=>e+t,0)-e.counts.reduce((e,t)=>e+t,0)||e.label.localeCompare(t.label);let f=g?[...p].sort(b).slice(0,i.topN):[...p];f.sort("countDesc"===i.sort?b:(e,t)=>h?Number(e.label)-Number(t.label):e.label.localeCompare(t.label));const v=f.map(e=>({...e,..."releaseYear"===i.dimension&&Number(e.label)===o?{partial:!0}:{},values:e.counts.map((e,t)=>"percent"===i.measure?d[t].total?100*e/d[t].total:0:e)}));("radar"===i.chartType&&v.length>20||"polarArea"===i.chartType&&v.length>30)&&fail("This radial chart has too many categories. Narrow the population or choose a bar chart.");const y=chartWarnings(i);return l&&y.push("The requested histogram range exceeds 1,000 bins. The axis shows the observed data range instead; zero-only boundary bins were not extended. Increase the bin width or narrow the range to display every requested bin."),g&&p.length>v.length&&y.push(`Showing the ${v.length} largest of ${p.length}${c?"+":""} categories returned. Omitted categories are not combined into an invented “Other” count.`),c&&y.push("The category aggregation reached its 1,000-bucket limit. Narrow the filters for a more complete category listing."),["pie","doughnut"].includes(i.chartType)&&(c||v.length!==p.length)&&fail("A pie or doughnut chart must show all categories. Increase the category limit or use a bar chart."),["pie","doughnut"].includes(i.chartType)&&v.reduce((e,t)=>e+t.counts[0],0)!==d[0].total&&fail("A pie or doughnut chart requires complete, non-overlapping categories covering every matching entry. Use a bar chart for this population."),{rows:v,series:d.map(({values:e,...t})=>t),warnings:y,retrievedAt:s,categoryCount:p.length}}module.exports={catalog,validateSchema,validateChartSpec,getChartPresentation,buildChartRequests,buildChartBaselineRequests,estimateChartRequests,chartBinBounds,formatChartBucketLabel,buildChartDrilldownRequest,describeFilter,describeChart,chartWarnings,chartChanges,formatChartData,isObservationChart,chartMetricLabel,buildObservationRequests,buildObservationDataRequest,formatObservationData};
|
package/build/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="RCSB Protein Data Bank: A Resource for Studying Biological Macromolecules"/><title>RCSB Statistics</title><script defer="defer" src="/static/js/main.
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="RCSB Protein Data Bank: A Resource for Studying Biological Macromolecules"/><title>RCSB Statistics</title><script defer="defer" src="/static/js/main.734f05c1.js"></script><link href="/static/css/main.894a09f3.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript sto run this app.</noscript><div id="statsroot"></div></body></html>
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
:where(#statsroot) .ai-chart-eyebrow { text-transform: uppercase; letter-spacing: .08em; color: #587b98; font-size: 11px; font-weight: 700; }
|
|
6
6
|
:where(#statsroot) .ai-chart-layout { display: grid; grid-template-columns: minmax(310px, 370px) minmax(0, 1fr); gap: 22px; align-items: start; }
|
|
7
7
|
:where(#statsroot) .ai-chart-chat { border: 1px solid var(--ai-border); border-radius: 9px; overflow: hidden; background: #fff; box-shadow: 0 3px 12px #28435b08; }
|
|
8
|
+
:where(#statsroot) .ai-chart-chat-disabled { background: #f1f3f5; border-color: #d2d5d8; box-shadow: none; color: #62676c; filter: grayscale(1); }
|
|
9
|
+
:where(#statsroot) .ai-chart-chat-disabled .ai-chart-panel-heading, :where(#statsroot) .ai-chart-chat-disabled .ai-chart-composer { background: #e9ecef; border-color: #d2d5d8; }
|
|
10
|
+
:where(#statsroot) .ai-chart-chat-disabled .ai-chart-message { background: #e1e4e7; color: #565b60; }
|
|
11
|
+
:where(#statsroot) .ai-chart-chat-disabled .btn:disabled, :where(#statsroot) .ai-chart-chat-disabled textarea:disabled, :where(#statsroot) .ai-chart-examples button:disabled { background: #e9ecef; border-color: #d2d5d8; color: #62676c; cursor: not-allowed; opacity: 1; }
|
|
8
12
|
:where(#statsroot) .ai-chart-panel-heading { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; background: #f3f7fa; border-bottom: 1px solid var(--ai-border); }
|
|
9
13
|
:where(#statsroot) .ai-chart-panel-heading strong { display: flex; gap: 7px; align-items: center; }
|
|
10
14
|
:where(#statsroot) .ai-chart-panel-heading .btn { font-size: 12px; text-decoration: none; }
|
|
@@ -14,7 +18,7 @@
|
|
|
14
18
|
:where(#statsroot) .ai-chart-muted { font-size: 12px; line-height: 1.5; color: #647787; margin: 0; }
|
|
15
19
|
:where(#statsroot) .ai-chart-examples { display: grid; gap: 7px; }
|
|
16
20
|
:where(#statsroot) .ai-chart-examples button { text-align: left; border: 1px solid #dce5ec; background: #fff; border-radius: 6px; padding: 8px 10px; color: #355774; line-height: 1.4; transition: background .15s, border-color .15s; }
|
|
17
|
-
:where(#statsroot) .ai-chart-examples button:hover { background: #f0f7fc; border-color: #83accd; }
|
|
21
|
+
:where(#statsroot) .ai-chart-examples button:hover:not(:disabled) { background: #f0f7fc; border-color: #83accd; }
|
|
18
22
|
:where(#statsroot) .ai-chart-examples strong { display: block; font-size: 12px; margin-bottom: 4px; }
|
|
19
23
|
:where(#statsroot) .ai-chart-examples span { display: block; color: #6b7b87; font-size: 12px; }
|
|
20
24
|
:where(#statsroot) .ai-chart-message { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; line-height: 1.55; border-radius: 8px; padding: 11px 12px; max-width: 95%; }
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
:where(#statsroot) .ai-chart-eyebrow { text-transform: uppercase; letter-spacing: .08em; color: #587b98; font-size: 11px; font-weight: 700; }
|
|
42
42
|
:where(#statsroot) .ai-chart-layout { display: grid; grid-template-columns: minmax(310px, 370px) minmax(0, 1fr); gap: 22px; align-items: start; }
|
|
43
43
|
:where(#statsroot) .ai-chart-chat { border: 1px solid var(--ai-border); border-radius: 9px; overflow: hidden; background: #fff; box-shadow: 0 3px 12px #28435b08; }
|
|
44
|
+
:where(#statsroot) .ai-chart-chat-disabled { background: #f1f3f5; border-color: #d2d5d8; box-shadow: none; color: #62676c; filter: grayscale(1); }
|
|
45
|
+
:where(#statsroot) .ai-chart-chat-disabled .ai-chart-panel-heading, :where(#statsroot) .ai-chart-chat-disabled .ai-chart-composer { background: #e9ecef; border-color: #d2d5d8; }
|
|
46
|
+
:where(#statsroot) .ai-chart-chat-disabled .ai-chart-message { background: #e1e4e7; color: #565b60; }
|
|
47
|
+
:where(#statsroot) .ai-chart-chat-disabled .btn:disabled, :where(#statsroot) .ai-chart-chat-disabled textarea:disabled, :where(#statsroot) .ai-chart-examples button:disabled { background: #e9ecef; border-color: #d2d5d8; color: #62676c; cursor: not-allowed; opacity: 1; }
|
|
44
48
|
:where(#statsroot) .ai-chart-panel-heading { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; background: #f3f7fa; border-bottom: 1px solid var(--ai-border); }
|
|
45
49
|
:where(#statsroot) .ai-chart-panel-heading strong { display: flex; gap: 7px; align-items: center; }
|
|
46
50
|
:where(#statsroot) .ai-chart-panel-heading .btn { font-size: 12px; text-decoration: none; }
|
|
@@ -50,7 +54,7 @@
|
|
|
50
54
|
:where(#statsroot) .ai-chart-muted { font-size: 12px; line-height: 1.5; color: #647787; margin: 0; }
|
|
51
55
|
:where(#statsroot) .ai-chart-examples { display: grid; gap: 7px; }
|
|
52
56
|
:where(#statsroot) .ai-chart-examples button { text-align: left; border: 1px solid #dce5ec; background: #fff; border-radius: 6px; padding: 8px 10px; color: #355774; line-height: 1.4; transition: background .15s, border-color .15s; }
|
|
53
|
-
:where(#statsroot) .ai-chart-examples button:hover { background: #f0f7fc; border-color: #83accd; }
|
|
57
|
+
:where(#statsroot) .ai-chart-examples button:hover:not(:disabled) { background: #f0f7fc; border-color: #83accd; }
|
|
54
58
|
:where(#statsroot) .ai-chart-examples strong { display: block; font-size: 12px; margin-bottom: 4px; }
|
|
55
59
|
:where(#statsroot) .ai-chart-examples span { display: block; color: #6b7b87; font-size: 12px; }
|
|
56
60
|
:where(#statsroot) .ai-chart-message { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; line-height: 1.55; border-radius: 8px; padding: 11px 12px; max-width: 95%; }
|