@rcsb/rcsb-statistics 0.3.44-dev.aipcb.3 → 0.3.44-dev.aipcb.5

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/README.md CHANGED
@@ -38,9 +38,13 @@ The standalone chart workspace is available at `/ai-powered-chart-builder` benea
38
38
 
39
39
  Sierra supplies `window.__RCSB_AI_COMMANDS__` (or `sierraConfig.aiCommands`) with `execute(request, { signal })`, asynchronous `getStatus()`, and `settingsUrl`. Requests use action `configureChart`, conversation `messages`, validated `context.currentChart` / `context.pendingChart`, and `previousCommand`. Provider credentials are owned by Sierra; this package neither stores credentials nor calls model providers.
40
40
 
41
- The package includes `build/ai/chart-catalog.json` and `build/ai/chart-core.js`. The server and browser share the same schema, validation, Search API request construction, and data formatting. A ready response includes `command: { action: 'configureChart', spec }` plus `preview.chartData`, populated by the server from real Search API aggregations. The browser rejects incomplete or inconsistent preview data.
41
+ The package includes `build/ai/chart-catalog.json` and `build/ai/chart-core.js`. The server and browser share the same schema, validation, fixed API request construction, and data formatting. A ready response includes `command: { action: 'configureChart', spec }` plus `preview.chartData`, populated by the server from real Search API aggregations or paired Data API entry records. The browser rejects incomplete or inconsistent preview data. Model output configures the chart; it never supplies measurements or executable Chart.js options.
42
42
 
43
- Supported dimensions are release year, experimental method, source organism, resolution, polymer type, integrative input data, and determination methodology. All measures count released PDB entries, not chains or unique sequences. Up to four named cohorts can add entry-level filters. Overlap, missing-value denominators, current-year coverage, category limits, and cumulative-window semantics are disclosed; unsupported statistics require clarification.
43
+ Chart types include bar, line, area, pie, doughnut, radar, polar area, mixed bar/line/area, scatter, and bubble. Collapsed customization controls adjust labels, colors, typography, axes, line/point/bar styles, radial settings, and valid stacking without another AI request. Data-changing refinements require a new preview.
44
+
45
+ Aggregate dimensions cover release year, experimental method, source organism, resolution, polymer type, integrative input data, determination methodology, deposited molecular weight, heavy-atom records, polymer monomers, polymer chains, and distinct polymer entities. Their values count released PDB entries, not chains or unique sequences. Up to four named cohorts can add entry-level filters. Strict numeric bounds retain their exact meaning. Overlap, missing-value denominators, current-year coverage, category limits, and cumulative-window semantics are disclosed; unsupported statistics require clarification.
46
+
47
+ Scatter/bubble specifications add `observations: { x, y, size, limit }` using catalog metrics (`size: null` for scatter). The server selects at most 500 entries across cohorts, ordered by newest initial release and entry ID, then retrieves their metrics in one fixed GraphQL request. This bounded selection is not a representative random sample. Resolution means minimum positive reported resolution; deposited mass excludes solvent; total polymer monomers do not mean an individual protein's sequence length. Missing values are omitted, never imputed. Record results include `kind: 'records'`, `points`, `selectedEntryIds`, and per-cohort total/selected/omitted counts. Exports retain measurement units, exact identifiers, and retrieval scope. Existing version-1 aggregate specifications remain supported.
44
48
 
45
49
  Run `npm test`, `npx tsc --noEmit`, and `npm run build` before packaging. The tests use mocked model/service responses; they do not spend provider credits.
46
50
 
@@ -2,64 +2,92 @@
2
2
  "version": 1,
3
3
  "action": "configureChart",
4
4
  "name": "AI Powered Chart Builder",
5
- "checkedAt": "2026-09-13",
6
- "sources": ["https://search.rcsb.org/#faceted-queries", "https://search.rcsb.org/rcsbsearch/v2/metadata/schema"],
5
+ "checkedAt": "2026-09-15",
6
+ "sources": ["https://search.rcsb.org/#faceted-queries", "https://search.rcsb.org/rcsbsearch/v2/metadata/schema", "https://data.rcsb.org/", "https://data.rcsb.org/graphql"],
7
7
  "countingRules": [
8
- "Every value is a live count of released PDB entries, not polymer entities, atoms, chains, downloads, citations, or unique sequences. Computed models are excluded.",
8
+ "Aggregation values are live counts of released PDB entries, not polymer entities, atoms, chains, downloads, citations, or unique sequences. Scatter and bubble points instead pair catalog measurements from the same identified entry. Computed models are excluded.",
9
9
  "Categories may overlap when an entry has multiple methods, organisms, polymer types, input datasets, or resolution values. Do not sum overlapping buckets to claim a unique total.",
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
- "Lower and upper bounds on one numeric or date attribute are combined into one inclusive interval. At least one value must satisfy both bounds; different reported resolutions cannot satisfy opposite ends of the interval.",
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
13
  "Cumulative release-year counts start at the selected lower release-year bound, not necessarily the beginning of the archive. Current-year counts are incomplete.",
14
- "Area charts require an ordered dimension, ascending labels, vertical orientation and linear scale. Their translucent series are not stacked. Doughnut charts follow the same complete, non-overlapping partition rules as pie charts.",
15
- "Presentation settings change appearance only. They cannot change chart counts, filters, aggregation attributes, or destinations. Use only the supported palettes, legend positions, grid, line styles and numeric drawing controls.",
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
+ "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.",
18
- "Ask a clarification when a request needs unsupported statistics, ambiguous units or entity relationships, or an unknown organism taxon. Preserve the existing chart and filters during refinement unless the user requests a change."
18
+ "Ask a clarification when a request needs unsupported statistics, ambiguous units or entity relationships, or an unknown organism taxon. Preserve the existing chart and filters during refinement unless the user requests a change.",
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
+ "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
+ "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
+ "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."
19
23
  ],
20
24
  "dimensions": {
21
25
  "releaseYear": {"label": "Release year", "attribute": "rcsb_accession_info.initial_release_date", "aggregation": "date_histogram", "interval": "year", "ordered": true, "overlapping": false},
22
26
  "method": {"label": "Experimental method", "attribute": "exptl.method", "aggregation": "terms", "ordered": false, "overlapping": true},
23
27
  "sourceOrganism": {"label": "Source organism", "attribute": "rcsb_entity_source_organism.ncbi_scientific_name", "aggregation": "terms", "ordered": false, "overlapping": true},
24
- "resolution": {"label": "Resolution (Å)", "attribute": "rcsb_entry_info.resolution_combined", "aggregation": "histogram", "ordered": true, "overlapping": true},
28
+ "resolution": {"label": "Resolution (Å)", "unit": "Å", "attribute": "rcsb_entry_info.resolution_combined", "aggregation": "histogram", "ordered": true, "overlapping": true, "defaultInterval": 0.5},
29
+ "molecularWeight": {"label": "Deposited molecular mass (kDa)", "unit": "kDa", "attribute": "rcsb_entry_info.molecular_weight", "aggregation": "histogram", "ordered": true, "overlapping": false, "defaultInterval": 10},
30
+ "atomCount": {"label": "Deposited heavy-atom count", "unit": "atoms", "attribute": "rcsb_entry_info.deposited_atom_count", "aggregation": "histogram", "ordered": true, "overlapping": false, "integer": true, "defaultInterval": 1000},
31
+ "monomerCount": {"label": "Deposited polymer monomer count", "unit": "monomers", "attribute": "rcsb_entry_info.deposited_polymer_monomer_count", "aggregation": "histogram", "ordered": true, "overlapping": false, "integer": true, "defaultInterval": 100},
32
+ "chainCount": {"label": "Deposited polymer-chain count", "unit": "chains", "attribute": "rcsb_entry_info.deposited_polymer_entity_instance_count", "aggregation": "histogram", "ordered": true, "overlapping": false, "integer": true, "defaultInterval": 1},
33
+ "polymerEntityCount": {"label": "Distinct polymer-entity count", "unit": "entities", "attribute": "rcsb_entry_info.polymer_entity_count", "aggregation": "histogram", "ordered": true, "overlapping": false, "integer": true, "defaultInterval": 1},
25
34
  "polymerType": {"label": "Polymer type", "attribute": "entity_poly.rcsb_entity_polymer_type", "aggregation": "terms", "ordered": false, "overlapping": true},
26
35
  "integrativeInput": {"label": "Integrative input data type", "attribute": "rcsb_ihm_dataset_list.name", "aggregation": "terms", "ordered": false, "overlapping": true, "requiredFilter": {"field": "methodology", "operator": "in", "value": ["integrative"]}},
27
36
  "methodology": {"label": "Structure determination methodology", "attribute": "rcsb_entry_info.structure_determination_methodology", "aggregation": "terms", "ordered": false, "overlapping": false}
28
37
  },
29
38
  "filters": {
30
- "releaseYear": {"label": "Release year", "attribute": "rcsb_accession_info.initial_release_date", "kind": "year", "operators": ["gte", "lte"], "minimum": 1971, "maximum": 2100},
39
+ "releaseYear": {"label": "Release year", "attribute": "rcsb_accession_info.initial_release_date", "kind": "year", "operators": ["gte", "lte", "gt", "lt"], "minimum": 1971, "maximum": 2100},
31
40
  "method": {"label": "Experimental method", "attribute": "exptl.method", "kind": "enum", "operators": ["in"], "values": ["X-RAY DIFFRACTION", "ELECTRON MICROSCOPY", "SOLUTION NMR", "SOLID-STATE NMR", "NEUTRON DIFFRACTION", "ELECTRON CRYSTALLOGRAPHY", "FIBER DIFFRACTION", "POWDER DIFFRACTION", "SOLUTION SCATTERING", "INFRARED SPECTROSCOPY", "EPR", "FLUORESCENCE TRANSFER"]},
32
- "resolution": {"label": "Resolution (Å)", "attribute": "rcsb_entry_info.resolution_combined", "kind": "number", "operators": ["gte", "lte"], "minimum": 0.01, "maximum": 1000},
41
+ "resolution": {"label": "Resolution (Å)", "attribute": "rcsb_entry_info.resolution_combined", "kind": "number", "operators": ["gte", "lte", "gt", "lt"], "minimum": 0.01, "maximum": 1000},
42
+ "molecularWeight": {"label": "Deposited molecular mass (kDa)", "attribute": "rcsb_entry_info.molecular_weight", "kind": "number", "operators": ["gte", "lte", "gt", "lt"], "minimum": 0, "maximum": 1000000000},
43
+ "atomCount": {"label": "Deposited heavy-atom count", "attribute": "rcsb_entry_info.deposited_atom_count", "kind": "number", "integer": true, "operators": ["gte", "lte", "gt", "lt"], "minimum": 0, "maximum": 1000000000000},
44
+ "monomerCount": {"label": "Deposited polymer monomer count", "attribute": "rcsb_entry_info.deposited_polymer_monomer_count", "kind": "number", "integer": true, "operators": ["gte", "lte", "gt", "lt"], "minimum": 0, "maximum": 1000000000000},
45
+ "chainCount": {"label": "Deposited polymer-chain count", "attribute": "rcsb_entry_info.deposited_polymer_entity_instance_count", "kind": "number", "integer": true, "operators": ["gte", "lte", "gt", "lt"], "minimum": 0, "maximum": 1000000000},
46
+ "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},
33
47
  "sourceTaxonomy": {"label": "Source organism NCBI taxonomy ID (including descendants)", "attribute": "rcsb_entity_source_organism.taxonomy_lineage.id", "kind": "taxonomy", "operators": ["in"]},
34
48
  "sourceOrganism": {"label": "Exact source organism scientific name", "attribute": "rcsb_entity_source_organism.ncbi_scientific_name", "kind": "text", "operators": ["in"]},
35
- "ligand": {"label": "Non-polymer chemical component ID present in entry", "attribute": "rcsb_nonpolymer_entity.rcsb_nonpolymer_comp_id", "kind": "identifier", "operators": ["in"]},
49
+ "ligand": {"label": "Non-polymer chemical component ID present in entry", "attribute": "rcsb_nonpolymer_entity_container_identifiers.nonpolymer_comp_id", "kind": "identifier", "operators": ["in"]},
36
50
  "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"]},
37
51
  "methodology": {"label": "Structure determination methodology", "attribute": "rcsb_entry_info.structure_determination_methodology", "kind": "enum", "operators": ["in"], "values": ["experimental", "integrative"]}
38
52
  },
53
+ "metrics": {
54
+ "resolution": {"label": "Minimum reported resolution", "unit": "Å", "attribute": "rcsb_entry_info.resolution_combined", "dataField": "rcsb_entry_info.resolution_combined", "description": "Minimum positive combined experimental resolution reported for the entry. Multiple methods can report multiple values; this is not their average or necessarily the value satisfying a population filter."},
55
+ "molecularWeight": {"label": "Deposited molecular mass", "unit": "kDa", "attribute": "rcsb_entry_info.molecular_weight", "dataField": "rcsb_entry_info.molecular_weight", "description": "Molecular mass of polymer and non-polymer entities in the deposited entry, excluding solvent; not the mass of a selected polymer or biological assembly."},
56
+ "atomCount": {"label": "Deposited heavy-atom count", "unit": "atoms", "attribute": "rcsb_entry_info.deposited_atom_count", "dataField": "rcsb_entry_info.deposited_atom_count", "description": "Number of heavy-atom coordinate records per deposited structure model."},
57
+ "monomerCount": {"label": "Deposited polymer monomer count", "unit": "monomers", "attribute": "rcsb_entry_info.deposited_polymer_monomer_count", "dataField": "rcsb_entry_info.deposited_polymer_monomer_count", "description": "Total polymer monomers across sample polymer instances per deposited structure model, including unmodeled monomers; not a single-chain sequence length."},
58
+ "chainCount": {"label": "Deposited polymer-chain count", "unit": "chains", "attribute": "rcsb_entry_info.deposited_polymer_entity_instance_count", "dataField": "rcsb_entry_info.deposited_polymer_entity_instance_count", "description": "Number of polymer entity instances per deposited structure model; not the number of distinct polymer entities or biological assembly chains."},
59
+ "polymerEntityCount": {"label": "Distinct polymer-entity count", "unit": "entities", "attribute": "rcsb_entry_info.polymer_entity_count", "dataField": "rcsb_entry_info.polymer_entity_count", "description": "Number of chemically distinct polymer entities in the entry, not copies of those entities."},
60
+ "releaseYear": {"label": "Initial release year", "unit": "year", "attribute": "rcsb_accession_info.initial_release_date", "dataField": "rcsb_accession_info.initial_release_date", "description": "UTC calendar year of the entry's initial PDB release, not deposition or latest revision."}
61
+ },
39
62
  "knownTaxa": {"human": 9606, "mouse": 10090, "Escherichia coli": 562, "Saccharomyces cerevisiae": 4932},
40
- "presentationDefaults": {"palette": "default", "legend": "auto", "grid": true, "lineStyle": "smooth", "pointRadius": 3, "borderWidth": 1, "barRadius": 0, "doughnutCutout": 60},
63
+ "presentationDefaults": {"palette": "default", "legend": "auto", "grid": true, "lineStyle": "smooth", "pointRadius": 3, "borderWidth": 1, "barRadius": 0, "doughnutCutout": 60, "colors": [], "fillOpacity": 0.25, "pointStyle": "circle", "lineDash": "solid", "barWidth": 0.8, "tickRotation": 55, "fontSize": 12, "height": 400, "radialStartAngle": 0, "xScale": "linear", "xMin": null, "xMax": null, "yMin": null, "yMax": null, "reverseX": false, "reverseY": false, "seriesTypes": [], "stacking": "grouped"},
41
64
  "specSchema": {
42
65
  "type": "object", "additionalProperties": false,
43
66
  "required": ["version", "title", "categoryLabel", "valueLabel", "dimension", "chartType", "measure", "scale", "orientation", "sort", "topN", "interval", "cumulative", "filters", "series"],
44
67
  "properties": {
45
68
  "version": {"type": "integer", "enum": [1]},
46
69
  "title": {"type": "string", "minLength": 1, "maxLength": 140},
47
- "categoryLabel": {"type": "string", "minLength": 1, "maxLength": 100},
48
- "valueLabel": {"type": "string", "minLength": 1, "maxLength": 100},
49
- "dimension": {"type": "string", "enum": ["releaseYear", "method", "sourceOrganism", "resolution", "polymerType", "integrativeInput", "methodology"]},
50
- "chartType": {"type": "string", "enum": ["bar", "line", "area", "pie", "doughnut"]},
70
+ "categoryLabel": {"type": "string", "maxLength": 100},
71
+ "valueLabel": {"type": "string", "maxLength": 100},
72
+ "dimension": {"type": "string", "enum": ["releaseYear", "method", "sourceOrganism", "resolution", "polymerType", "integrativeInput", "methodology", "molecularWeight", "atomCount", "monomerCount", "chainCount", "polymerEntityCount"]},
73
+ "chartType": {"type": "string", "enum": ["bar", "line", "area", "pie", "doughnut", "radar", "polarArea", "mixed", "scatter", "bubble"]},
51
74
  "measure": {"type": "string", "enum": ["count", "percent"]},
52
75
  "scale": {"type": "string", "enum": ["linear", "logarithmic"]},
53
76
  "orientation": {"type": "string", "enum": ["vertical", "horizontal"]},
54
77
  "sort": {"type": "string", "enum": ["labelAsc", "countDesc"]},
55
78
  "topN": {"type": "integer", "minimum": 1, "maximum": 60},
56
- "interval": {"type": "number", "minimum": 0.1, "maximum": 100},
79
+ "interval": {"type": "number", "minimum": 0.1, "maximum": 100000},
57
80
  "cumulative": {"type": "boolean"},
58
81
  "presentation": {"$ref": "#/$defs/presentation"},
82
+ "observations": {"anyOf": [{"type": "null"}, {"type": "object", "additionalProperties": false, "required": ["x", "y", "size", "limit"], "properties": {
83
+ "x": {"$ref": "#/$defs/metric"}, "y": {"$ref": "#/$defs/metric"}, "size": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/metric"}]}, "limit": {"type": "integer", "minimum": 1, "maximum": 500}
84
+ }}]},
59
85
  "filters": {"type": "array", "maxItems": 12, "items": {"$ref": "#/$defs/filter"}},
60
86
  "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"}}}}}
61
87
  },
62
88
  "$defs": {
89
+ "metric": {"type": "string", "enum": ["resolution", "molecularWeight", "atomCount", "monomerCount", "chainCount", "polymerEntityCount", "releaseYear"]},
90
+ "axisBound": {"anyOf": [{"type": "number", "minimum": -1000000000000, "maximum": 1000000000000}, {"type": "null"}]},
63
91
  "presentation": {"type": "object", "additionalProperties": false, "properties": {
64
92
  "palette": {"type": "string", "enum": ["default", "accessible", "warm", "cool", "monochrome"]},
65
93
  "legend": {"type": "string", "enum": ["auto", "top", "bottom", "left", "right", "hidden"]},
@@ -68,9 +96,23 @@
68
96
  "pointRadius": {"type": "integer", "minimum": 0, "maximum": 8},
69
97
  "borderWidth": {"type": "integer", "minimum": 1, "maximum": 5},
70
98
  "barRadius": {"type": "integer", "minimum": 0, "maximum": 12},
71
- "doughnutCutout": {"type": "integer", "minimum": 20, "maximum": 85}
99
+ "doughnutCutout": {"type": "integer", "minimum": 20, "maximum": 85},
100
+ "colors": {"type": "array", "maxItems": 8, "items": {"type": "string", "pattern": "^#[0-9a-fA-F]{6}$"}},
101
+ "fillOpacity": {"type": "number", "minimum": 0, "maximum": 1},
102
+ "pointStyle": {"type": "string", "enum": ["circle", "rect", "triangle", "cross"]},
103
+ "lineDash": {"type": "string", "enum": ["solid", "dashed", "dotted"]},
104
+ "barWidth": {"type": "number", "minimum": 0.1, "maximum": 1},
105
+ "tickRotation": {"type": "integer", "minimum": 0, "maximum": 90},
106
+ "fontSize": {"type": "integer", "minimum": 10, "maximum": 20},
107
+ "height": {"type": "integer", "minimum": 280, "maximum": 800},
108
+ "radialStartAngle": {"type": "number", "minimum": 0, "maximum": 360},
109
+ "xScale": {"type": "string", "enum": ["linear", "logarithmic"]},
110
+ "xMin": {"$ref": "#/$defs/axisBound"}, "xMax": {"$ref": "#/$defs/axisBound"}, "yMin": {"$ref": "#/$defs/axisBound"}, "yMax": {"$ref": "#/$defs/axisBound"},
111
+ "reverseX": {"type": "boolean"}, "reverseY": {"type": "boolean"},
112
+ "seriesTypes": {"type": "array", "maxItems": 4, "items": {"type": "string", "enum": ["bar", "line", "area"]}},
113
+ "stacking": {"type": "string", "enum": ["grouped", "stacked"]}
72
114
  }},
73
- "filter": {"type": "object", "additionalProperties": false, "required": ["field", "operator", "value"], "properties": {"field": {"type": "string", "enum": ["releaseYear", "method", "resolution", "sourceTaxonomy", "sourceOrganism", "ligand", "polymerType", "methodology"]}, "operator": {"type": "string", "enum": ["in", "gte", "lte"]}, "value": {"anyOf": [{"type": "number"}, {"type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true, "items": {"anyOf": [{"type": "string", "minLength": 1, "maxLength": 120}, {"type": "integer", "minimum": 1, "maximum": 999999999}]}}]}}}
115
+ "filter": {"type": "object", "additionalProperties": false, "required": ["field", "operator", "value"], "properties": {"field": {"type": "string", "enum": ["releaseYear", "method", "resolution", "sourceTaxonomy", "sourceOrganism", "ligand", "polymerType", "methodology", "molecularWeight", "atomCount", "monomerCount", "chainCount", "polymerEntityCount"]}, "operator": {"type": "string", "enum": ["in", "gte", "lte", "gt", "lt"]}, "value": {"anyOf": [{"type": "number"}, {"type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true, "items": {"anyOf": [{"type": "string", "minLength": 1, "maxLength": 120}, {"type": "integer", "minimum": 1, "maximum": 999999999}]}}]}}}
74
116
  }
75
117
  }
76
118
  }
@@ -1,4 +1,7 @@
1
- export interface ChartFilter { field: string; operator: 'in' | 'gte' | 'lte'; value: number | Array<string | number>; }
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' | 'releaseYear';
3
+ export interface ChartObservation { x: ChartMetric; y: ChartMetric; size: ChartMetric | null; limit: number; }
4
+ export interface ChartPoint { entryId: string; seriesIndex: number; x: number; y: number; size?: number; }
2
5
  export interface ChartPresentation {
3
6
  palette: 'default' | 'accessible' | 'warm' | 'cool' | 'monochrome';
4
7
  legend: 'auto' | 'top' | 'bottom' | 'left' | 'right' | 'hidden';
@@ -8,14 +11,32 @@ export interface ChartPresentation {
8
11
  borderWidth: number;
9
12
  barRadius: number;
10
13
  doughnutCutout: number;
14
+ colors: string[];
15
+ fillOpacity: number;
16
+ pointStyle: 'circle' | 'rect' | 'triangle' | 'cross';
17
+ lineDash: 'solid' | 'dashed' | 'dotted';
18
+ barWidth: number;
19
+ tickRotation: number;
20
+ fontSize: number;
21
+ height: number;
22
+ radialStartAngle: number;
23
+ xScale: 'linear' | 'logarithmic';
24
+ xMin: number | null;
25
+ xMax: number | null;
26
+ yMin: number | null;
27
+ yMax: number | null;
28
+ reverseX: boolean;
29
+ reverseY: boolean;
30
+ seriesTypes: Array<'bar' | 'line' | 'area'>;
31
+ stacking: 'grouped' | 'stacked';
11
32
  }
12
33
  export interface ChartSpec {
13
34
  version: 1;
14
35
  title: string;
15
36
  categoryLabel: string;
16
37
  valueLabel: string;
17
- dimension: 'releaseYear' | 'method' | 'sourceOrganism' | 'resolution' | 'polymerType' | 'integrativeInput' | 'methodology';
18
- chartType: 'bar' | 'line' | 'area' | 'pie' | 'doughnut';
38
+ dimension: 'releaseYear' | 'method' | 'sourceOrganism' | 'resolution' | 'polymerType' | 'integrativeInput' | 'methodology' | 'molecularWeight' | 'atomCount' | 'monomerCount' | 'chainCount' | 'polymerEntityCount';
39
+ chartType: 'bar' | 'line' | 'area' | 'pie' | 'doughnut' | 'radar' | 'polarArea' | 'mixed' | 'scatter' | 'bubble';
19
40
  measure: 'count' | 'percent';
20
41
  scale: 'linear' | 'logarithmic';
21
42
  orientation: 'vertical' | 'horizontal';
@@ -24,25 +45,35 @@ export interface ChartSpec {
24
45
  interval: number;
25
46
  cumulative: boolean;
26
47
  presentation?: Partial<ChartPresentation>;
48
+ observations?: ChartObservation | null;
27
49
  filters: ChartFilter[];
28
50
  series: Array<{ label: string; filters: ChartFilter[] }>;
29
51
  }
30
52
  export interface ChartDataResult {
53
+ kind?: 'records';
31
54
  rows: Array<{ label: string; counts: number[]; values: number[] }>;
32
- series: Array<{ label: string; total: number }>;
55
+ points?: ChartPoint[];
56
+ selectedEntryIds?: string[][];
57
+ series: Array<{ label: string; total: number; selected?: number; omitted?: number }>;
33
58
  warnings: string[];
34
59
  retrievedAt: string;
35
60
  categoryCount: number;
36
61
  }
37
62
  export const catalog: {
38
63
  version: number;
39
- dimensions: Record<string, { label: string; attribute: string; aggregation: string; ordered: boolean; overlapping: boolean }>;
64
+ dimensions: Record<string, { label: string; unit?: string; attribute: string; aggregation: string; ordered: boolean; overlapping: boolean; integer?: boolean; defaultInterval?: number }>;
40
65
  filters: Record<string, { label: string; attribute: string }>;
66
+ metrics: Record<ChartMetric, { label: string; unit: string; attribute: string; dataField: string; description: string }>;
41
67
  specSchema: Record<string, unknown>;
42
68
  countingRules: string[];
43
69
  presentationDefaults: ChartPresentation;
44
70
  };
45
71
  export function validateChartSpec(input: unknown): ChartSpec;
72
+ export function isObservationChart(input: ChartSpec): boolean;
73
+ export function chartMetricLabel(metric: ChartMetric): string;
74
+ export function buildObservationRequests(input: ChartSpec): Array<Record<string, unknown>>;
75
+ export function buildObservationDataRequest(entryIds: string[]): { query: string; variables: { ids: string[] } };
76
+ export function formatObservationData(input: ChartSpec, selections: unknown[], entries: unknown[]): ChartDataResult;
46
77
  export function buildChartRequests(input: ChartSpec): Array<Record<string, unknown>>;
47
78
  export function getChartPresentation(input: ChartSpec): ChartPresentation;
48
79
  export function buildChartDrilldownRequest(input: ChartSpec, rowLabel: string, seriesIndex: number): Record<string, unknown>;
@@ -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)&&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.`),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.`)}))}function validateChartSpec(e){validateSchema(e,catalog.specSchema);const t=JSON.parse(JSON.stringify(e)),r=catalog.dimensions[t.dimension];return!["line","area"].includes(t.chartType)||r.ordered&&"vertical"===t.orientation&&"labelAsc"===t.sort||fail("Line and area charts require release year or resolution, vertical orientation, and ascending labels."),"area"===t.chartType&&"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 (release year or methodology), linear scale, and non-cumulative values."),!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&&!Number.isInteger(e.value))&&fail(`Invalid range for ${t.label}.`)})),t.series.forEach(e=>{const r=[...t.filters,...e.filters];["releaseYear","resolution"].forEach(e=>{const t=r.filter(t=>t.field===e&&"gte"===t.operator).map(e=>e.value),a=r.filter(t=>t.field===e&&"lte"===t.operator).map(e=>e.value);t.length&&a.length&&Math.max(...t)>Math.min(...a)&&fail(`The ${e} lower bound exceeds its upper bound.`)}),"integrativeInput"===t.dimension&&r.some(e=>"methodology"===e.field&&!e.value.includes("integrative"))&&fail("Integrative input charts cannot exclude integrative structures.")}),t}function getChartPresentation(e){const t=validateChartSpec(e);return{...catalog.presentationDefaults,borderWidth:["line","area"].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"===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"}[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 i=e.filter(e=>e.field===r.field),n=i.filter(e=>"gte"===e.operator).map(e=>e.value),o=i.filter(e=>"lte"===e.operator).map(e=>e.value);if(!n.length||!o.length)return[terminal({...r,operator:n.length?"gte":"lte",value:n.length?Math.max(...n):Math.min(...o)})];const l=Math.max(...n),s=Math.min(...o);return[{type:"terminal",service:"text",parameters:{attribute:a.attribute,operator:"range",value:{from:"year"===a.kind?`${l}-01-01`:l,to:"year"===a.kind?`${s}-12-31`:s,include_lower:!0,include_upper:!0}}}]})}function buildChartRequests(e){const t=validateChartSpec(e),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);(!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={attribute:catalog.dimensions[a.dimension].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`;i.operator=a.cumulative?"less":"range",i.value=a.cumulative?e:{from:`${t}-01-01`,to:e,include_lower:!0,include_upper:!1}}else if("resolution"===a.dimension){const e=Number(t);(!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(t)||!Number.isFinite(e)||e<0||e>catalog.filters.resolution.maximum||Math.abs(e/a.interval-Math.round(e/a.interval))>1e-8)&&fail("Select a valid resolution interval."),i.operator="range",i.value={from:e,to:Number((e+a.interval).toPrecision(12)),include_lower:!0,include_upper:!1}}const n=buildChartRequests(a)[r];return{return_type:"entry",query:{type:"group",logical_operator:"and",nodes:[...n.query?.nodes||[],{type:"group",logical_operator:"and",nodes:[{type:"terminal",service:"text",parameters:i}]}]},request_options:{results_content_type:["experimental"],paginate:{start:0,rows:25}}}}function describeFilter(e){return`${catalog.filters[e.field].label} ${"in"===e.operator?"is any of":"gte"===e.operator?"≥":"≤"} ${Array.isArray(e.value)?e.value.join(", "):e.value}`}function describeChart(e){return validateChartSpec(e),`${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=[];return 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&&t.push("Comparison cohorts may overlap; each is counted independently."),"sourceOrganism"===e.dimension&&t.push("Source organisms are those recorded for any matching entry; they are not expression hosts. All source types are included."),"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&&t.push("Area series use a zero baseline and are not stacked; overlapping cohorts are counted independently."),"logarithmic"===e.scale&&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."),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"},n=(e,t)=>"grid"===e?t?"shown":"hidden":"doughnutCutout"===e?`${t}%`:String(t);return Object.keys(a).filter(e=>r[e]!==a[e]).map(e=>`${i[e]}: ${n(e,r[e])} → ${n(e,a[e])}.`)}return"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);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 a=e.facets?.find(e=>"chart"===e.name)?.buckets;Array.isArray(a)||0===e.total_count||fail("The Search API did not return the requested chart aggregation."),(a||[]).length>=1e3&&(i=!0);const n=new Map;return(a||[]).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 a=String(t.label);if("releaseYear"===r.dimension){const e=a.match(/\d{4}/);e||fail("The Search API returned an invalid release year."),a=e[0]}"resolution"===r.dimension&&((!Number.isFinite(Number(a))||Number(a)<0)&&fail("The Search API returned an invalid resolution interval."),a=String(Number(Number(a).toPrecision(12)))),n.has(a)&&fail("The Search API returned duplicate chart buckets."),n.set(a,t.population)}),{label:r.series[t].label,total:e.total_count,values:n}});let o=[...new Set(n.flatMap(e=>[...e.values.keys()]))];const l=o.every(e=>Number.isFinite(Number(e)));if(o.sort((e,t)=>l?Number(e)-Number(t):e.localeCompare(t)),"releaseYear"===r.dimension&&o.length){const e=Number(o[0]),t=Number(o[o.length-1]);t-e>200&&fail("The Search API returned an unsupported year range."),o=Array.from({length:t-e+1},(t,r)=>String(e+r))}if("resolution"===r.dimension&&o.length){const e=Number(o[0]),t=Math.round((Number(o[o.length-1])-e)/r.interval)+1;t>1e3&&fail("This resolution histogram exceeds 1,000 bins. Increase the bin width or narrow the resolution range."),o=Array.from({length:t},(t,a)=>String(Number((e+a*r.interval).toPrecision(12))))}const s=o.map(e=>({label:e,counts:n.map(t=>t.values.get(e)||0)}));if(r.cumulative){const e=n.map(()=>0);s.forEach(t=>{t.counts=t.counts.map((t,r)=>e[r]+=t)})}"countDesc"===r.sort&&s.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?s.slice(0,r.topN):s).map(e=>({...e,values:e.counts.map((e,t)=>"percent"===r.measure?n[t].total?100*e/n[t].total:0:e)})),h=chartWarnings(r);return u&&s.length>c.length&&h.push(`Showing ${c.length} of ${s.length}${i?"+":""} categories returned. Omitted categories are not combined into an invented “Other” count.`),i&&h.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!==s.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:h,retrievedAt:(new Date).toISOString(),categoryCount:s.length}}module.exports={catalog,validateSchema,validateChartSpec,getChartPresentation,buildChartRequests,buildChartDrilldownRequest,describeFilter,describeChart,chartWarnings,chartChanges,formatChartData};
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),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){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),a=catalog.filters[e].integer,i=t.from+(a&&!t.include_lower?1:0),n=t.to-(a&&!t.include_upper?1:0);!(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.")}),"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.isSafeInteger(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=>{(!isObject(t)||!Number.isSafeInteger(t.population)||t.population<0||t.population>e.total_count||!["string","number"].includes(typeof t.label)||!String(t.label).trim()||String(t.label).length>300||/[\u0000-\u001f\u007f]/.test(String(t.label)))&&fail("The Search API returned an invalid chart bucket.");let i=String(t.label);if("releaseYear"===r.dimension){const e=i.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."),i=e[1]}"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)}),!a.overlapping&&[...s.values()].reduce((e,t)=>e+t,0)>e.total_count&&fail("The Search API returned non-overlapping bucket counts exceeding the matching entry total."),{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};
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.12a5c7bd.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>
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.606600a0.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%; }
@@ -6,6 +6,18 @@
6
6
  :where(#statsroot) .ai-chart-style-grid select, :where(#statsroot) .ai-chart-style-grid input { font-size: 12px; max-width: 100%; }
7
7
  :where(#statsroot) .ai-chart-style-grid .ai-chart-grid-toggle { flex-direction: row; align-items: center; align-self: end; min-height: 31px; }
8
8
  :where(#statsroot) .ai-chart-customize p { color: #74818c; line-height: 1.5; margin: 10px 0 0; }
9
+ :where(#statsroot) .ai-chart-control-group { border-top: 1px solid #e4ebf1; padding: 10px 0; }
10
+ :where(#statsroot) .ai-chart-control-group > summary { cursor: pointer; font-weight: 600; margin-bottom: 10px; }
11
+ :where(#statsroot) .ai-chart-control-group:first-child { border-top: 0; padding-top: 0; }
12
+ :where(#statsroot) .ai-chart-colors { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-top: 12px; }
13
+ :where(#statsroot) .ai-chart-colors label { display: flex; align-items: center; gap: 8px; max-width: 100%; overflow-wrap: anywhere; }
14
+ :where(#statsroot) .ai-chart-colors input { width: 38px; height: 28px; padding: 2px; border: 1px solid #bacbd8; border-radius: 4px; flex-shrink: 0; }
15
+ :where(#statsroot) .ai-chart-observations { padding: 10px 12px; margin-bottom: 14px; border-left: 3px solid #779dbb; background: #f5f8fa; color: #496174; font-size: 12px; line-height: 1.5; }
16
+ :where(#statsroot) .ai-chart-observations p { margin: 0 0 6px; }
17
+ :where(#statsroot) .ai-chart-observations p:last-child { margin-bottom: 0; }
18
+ :where(#statsroot) .ai-chart-selection dl { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 14px; margin: 8px 0; }
19
+ :where(#statsroot) .ai-chart-selection dt { font-weight: normal; }
20
+ :where(#statsroot) .ai-chart-selection dd { margin: 0; font-variant-numeric: tabular-nums; }
9
21
  :where(#statsroot) .ai-chart-zoom { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin: 0 0 10px; }
10
22
  :where(#statsroot) .ai-chart-zoom .btn { font-size: 11px; }
11
23
  :where(#statsroot) .ai-chart-zoom small { color: #718393; font-size: 11px; line-height: 1.4; }
@@ -29,6 +41,10 @@
29
41
  :where(#statsroot) .ai-chart-eyebrow { text-transform: uppercase; letter-spacing: .08em; color: #587b98; font-size: 11px; font-weight: 700; }
30
42
  :where(#statsroot) .ai-chart-layout { display: grid; grid-template-columns: minmax(310px, 370px) minmax(0, 1fr); gap: 22px; align-items: start; }
31
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; }
32
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); }
33
49
  :where(#statsroot) .ai-chart-panel-heading strong { display: flex; gap: 7px; align-items: center; }
34
50
  :where(#statsroot) .ai-chart-panel-heading .btn { font-size: 12px; text-decoration: none; }
@@ -38,7 +54,7 @@
38
54
  :where(#statsroot) .ai-chart-muted { font-size: 12px; line-height: 1.5; color: #647787; margin: 0; }
39
55
  :where(#statsroot) .ai-chart-examples { display: grid; gap: 7px; }
40
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; }
41
- :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; }
42
58
  :where(#statsroot) .ai-chart-examples strong { display: block; font-size: 12px; margin-bottom: 4px; }
43
59
  :where(#statsroot) .ai-chart-examples span { display: block; color: #6b7b87; font-size: 12px; }
44
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%; }