@mstone6969/graph 0.1.0 → 0.2.0
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 +111 -14
- package/dist/ExcelWriter-3ZSYUH4T.cjs +154 -0
- package/dist/GraphType.js +24 -38
- package/dist/PdfWriter-6HJFYMCO.cjs +205 -0
- package/dist/catalog-YWX6FHRM.cjs +8 -0
- package/dist/catalog.js +5 -1
- package/dist/charts/bars/index.js +2 -12
- package/dist/charts/comparison/EffectForestPlot.js +1 -1
- package/dist/charts/comparison/index.js +1 -8
- package/dist/charts/demography/Nomogram.js +3 -3
- package/dist/charts/density/HexbinChart.js +6 -4
- package/dist/charts/diagrams/ArchitectureCanvas.js +68 -0
- package/dist/charts/diagrams/ArchitectureDiagram.js +331 -0
- package/dist/charts/diagrams/ArchitectureLayout.js +106 -0
- package/dist/charts/diagrams/ArchitectureLinkLabels.js +78 -0
- package/dist/charts/diagrams/ArchitectureModel.js +108 -0
- package/dist/charts/diagrams/ArchitectureRouter.js +348 -0
- package/dist/charts/diagrams/ArchitectureSamples.js +312 -0
- package/dist/charts/diagrams/ArchitectureShapes.js +130 -0
- package/dist/charts/diagrams/ArchitectureText.js +25 -0
- package/dist/charts/diagrams/DiagramText.js +38 -0
- package/dist/charts/diagrams/EntityRelationshipDiagram.js +465 -0
- package/dist/charts/diagrams/ErCrowsFoot.js +33 -0
- package/dist/charts/diagrams/ErSamples.js +205 -0
- package/dist/charts/diagrams/ErSchema.js +122 -0
- package/dist/charts/diagrams/LogicCircuit.js +154 -0
- package/dist/charts/diagrams/LogicCircuitLayout.js +600 -0
- package/dist/charts/diagrams/LogicCircuitSamples.js +66 -0
- package/dist/charts/diagrams/LogicGateDiagram.js +216 -0
- package/dist/charts/diagrams/LogicGateSymbol.js +113 -0
- package/dist/charts/diagrams/NetworkTopologyDiagram.js +247 -0
- package/dist/charts/diagrams/SequenceDiagram.js +351 -0
- package/dist/charts/diagrams/SequenceLayout.js +422 -0
- package/dist/charts/diagrams/SequenceModel.js +221 -0
- package/dist/charts/diagrams/TopologyLayout.js +143 -0
- package/dist/charts/diagrams/architectureEnums.js +50 -0
- package/dist/charts/diagrams/index.js +26 -0
- package/dist/charts/diagrams/logicEnums.js +42 -0
- package/dist/charts/diagrams/sequenceEnums.js +40 -0
- package/dist/charts/distribution/HistogramChart.js +25 -3
- package/dist/charts/distribution/index.js +1 -3
- package/dist/charts/earthscience/HovmollerDiagram.js +477 -0
- package/dist/charts/earthscience/HovmollerEnums.js +42 -0
- package/dist/charts/earthscience/HovmollerField.js +180 -0
- package/dist/charts/earthscience/HovmollerSample.js +40 -0
- package/dist/charts/earthscience/HovmollerSpeed.js +71 -0
- package/dist/charts/earthscience/index.js +10 -2
- package/dist/charts/engineering/ConstellationDiagram.js +26 -6
- package/dist/charts/engineering/EyeDiagram.js +2 -1
- package/dist/charts/engineering/PoleZeroPlot.js +32 -8
- package/dist/charts/financial/RelativeStrengthRotation.js +434 -0
- package/dist/charts/financial/RelativeStrengthRotationMath.js +71 -0
- package/dist/charts/financial/RelativeStrengthRotationSamples.js +27 -0
- package/dist/charts/financial/index.js +5 -3
- package/dist/charts/flow/index.js +1 -5
- package/dist/charts/frameworks/AnsoffMatrix.js +523 -0
- package/dist/charts/frameworks/FrameworkText.js +98 -0
- package/dist/charts/frameworks/NpsGauge.js +279 -0
- package/dist/charts/frameworks/NpsSurvey.js +58 -0
- package/dist/charts/frameworks/PerceptualMap.js +419 -0
- package/dist/charts/frameworks/PerceptualProjection.js +142 -0
- package/dist/charts/frameworks/PositioningSamples.js +191 -0
- package/dist/charts/frameworks/RaciMatrix.js +458 -0
- package/dist/charts/frameworks/RaciPlan.js +81 -0
- package/dist/charts/frameworks/RaciSamples.js +76 -0
- package/dist/charts/frameworks/SwotMatrix.js +358 -0
- package/dist/charts/frameworks/SwotSamples.js +85 -0
- package/dist/charts/frameworks/VanWestendorpChart.js +187 -0
- package/dist/charts/frameworks/VanWestendorpSurvey.js +197 -0
- package/dist/charts/frameworks/index.js +26 -0
- package/dist/charts/frameworks/raciEnums.js +30 -0
- package/dist/charts/frameworks/swotEnums.js +34 -0
- package/dist/charts/genomics/CircosPlot.js +1 -1
- package/dist/charts/geoflows/GeoFlowChart.js +2 -2
- package/dist/charts/geoflows/GeoFlowSample.js +2 -1
- package/dist/charts/geomaps/FloorPlanMap.js +324 -0
- package/dist/charts/geomaps/FloorPlanModel.js +179 -0
- package/dist/charts/geomaps/FloorPlanSamples.js +416 -0
- package/dist/charts/geomaps/GeoLand.js +2 -2
- package/dist/charts/geomaps/GeoSamples.js +2 -1
- package/dist/charts/geomaps/GraticuleMap.js +2 -2
- package/dist/charts/geomaps/RoomLabelFit.js +200 -0
- package/dist/charts/geomaps/TissotMap.js +2 -2
- package/dist/charts/geomaps/index.js +8 -4
- package/dist/charts/hierarchy/SunburstChart.js +17 -10
- package/dist/charts/hierarchy/index.js +2 -11
- package/dist/charts/indicators/BulletChart.js +8 -2
- package/dist/charts/indicators/SpeedometerChart.js +1 -1
- package/dist/charts/indicators/ThermometerChart.js +1 -1
- package/dist/charts/lifescience/LoessFit.js +113 -0
- package/dist/charts/lifescience/MaPlot.js +337 -0
- package/dist/charts/lifescience/MaPlotSamples.js +35 -0
- package/dist/charts/lifescience/OncoGlyph.js +137 -0
- package/dist/charts/lifescience/OncoPrint.js +373 -0
- package/dist/charts/lifescience/OncoPrintMatrix.js +107 -0
- package/dist/charts/lifescience/OncoPrintSamples.js +142 -0
- package/dist/charts/lifescience/PedigreeChart.js +306 -0
- package/dist/charts/lifescience/PedigreeLayout.js +261 -0
- package/dist/charts/lifescience/PedigreeRows.js +70 -0
- package/dist/charts/lifescience/PedigreeSamples.js +161 -0
- package/dist/charts/lifescience/PedigreeSymbol.js +47 -0
- package/dist/charts/lifescience/RamachandranPlot.js +364 -0
- package/dist/charts/lifescience/RamachandranRegions.js +2712 -0
- package/dist/charts/lifescience/RamachandranSamples.js +235 -0
- package/dist/charts/lifescience/index.js +25 -0
- package/dist/charts/lifescience/pedigreeEnums.js +21 -0
- package/dist/charts/lifescience/ramachandranEnums.js +34 -0
- package/dist/charts/linesareas/DualAxisChart.js +1 -1
- package/dist/charts/linesareas/LineEnums.js +1 -15
- package/dist/charts/linesareas/MultiLineChart.js +1 -1
- package/dist/charts/linesareas/TrendChart.js +15 -5
- package/dist/charts/linesareas/index.js +2 -6
- package/dist/charts/massive/MassiveChart.js +3 -2
- package/dist/charts/mathematical/FourierSeriesChart.js +7 -1
- package/dist/charts/mathematical/index.js +2 -6
- package/dist/charts/matrix/GridPlot.js +27 -2
- package/dist/charts/matrix/TilegramChart.js +2 -2
- package/dist/charts/mlcharts/AttentionGrid.js +79 -0
- package/dist/charts/mlcharts/AttentionHeatmap.js +329 -0
- package/dist/charts/mlcharts/AttentionModel.js +132 -0
- package/dist/charts/mlcharts/AttentionSamples.js +82 -0
- package/dist/charts/mlcharts/CellNumber.js +8 -0
- package/dist/charts/mlcharts/CurveDiagnosticSamples.js +95 -0
- package/dist/charts/mlcharts/CurvePickMark.js +31 -0
- package/dist/charts/mlcharts/DecisionBoundaryChart.js +316 -0
- package/dist/charts/mlcharts/DecisionSamples.js +60 -0
- package/dist/charts/mlcharts/DecisionSurface.js +130 -0
- package/dist/charts/mlcharts/DistinctTicks.js +13 -0
- package/dist/charts/mlcharts/ElbowMath.js +153 -0
- package/dist/charts/mlcharts/ElbowPlot.js +273 -0
- package/dist/charts/mlcharts/ExplainabilitySamples.js +70 -0
- package/dist/charts/mlcharts/FeatureImportanceChart.js +150 -0
- package/dist/charts/mlcharts/ImportanceRanking.js +98 -0
- package/dist/charts/mlcharts/LearningCurveChart.js +276 -0
- package/dist/charts/mlcharts/LearningCurveMath.js +92 -0
- package/dist/charts/mlcharts/PartialDependence.js +92 -0
- package/dist/charts/mlcharts/PartialDependencePlot.js +225 -0
- package/dist/charts/mlcharts/WideLabel.js +28 -0
- package/dist/charts/mlcharts/curveDiagnosticEnums.js +37 -0
- package/dist/charts/mlcharts/index.js +32 -0
- package/dist/charts/proportion/index.js +5 -9
- package/dist/charts/scatter/CategoryScatterBase.js +1 -4
- package/dist/charts/scatter/ConnectedScatter.js +7 -4
- package/dist/charts/scatter/MarginalScatter.js +13 -8
- package/dist/charts/scatter/index.js +4 -17
- package/dist/charts/scientific/SpectrogramChart.js +9 -3
- package/dist/charts/scientific/index.js +2 -2
- package/dist/charts/signals/index.js +3 -13
- package/dist/charts/statistics/LorenzCurve.js +7 -4
- package/dist/charts/threebars/Candlestick3DChart.js +18 -2
- package/dist/charts/threeglobe/GlobeChart.js +2 -2
- package/dist/charts/threeglobe/GlobeSamples.js +2 -1
- package/dist/charts/threeglobe/RingGlobe.js +1 -1
- package/dist/charts/threenetwork/ConeTreeChart.js +2 -1
- package/dist/charts/threenetwork/Gantt3DChart.js +3 -2
- package/dist/charts/threesurfaces/Ridgeline3DChart.js +22 -4
- package/dist/charts/threevolume/LorenzAttractor.js +19 -0
- package/dist/charts/threevolume/VolumeRaycastChart.js +4 -3
- package/dist/charts/threevolume/volumeEnums.js +1 -0
- package/dist/charts/timeline/index.js +2 -10
- package/dist/chunk-753UBLV2.cjs +33 -0
- package/dist/chunk-VX54A7KA.cjs +17 -0
- package/dist/{chunk-W5GAAAZY.cjs → chunk-WAODDPYX.cjs} +24874 -17620
- package/dist/chunk-Z3JPIJSC.cjs +338 -0
- package/dist/core/CartesianChart.js +87 -11
- package/dist/core/Chart.js +63 -15
- package/dist/core/Chart2D.js +54 -17
- package/dist/core/Chart3D.js +56 -20
- package/dist/core/ChartExportError.js +2 -0
- package/dist/core/ChartExporter.js +50 -8
- package/dist/core/ColumnCheck.js +53 -0
- package/dist/core/Context2D.js +29 -15
- package/dist/core/Context3D.js +7 -2
- package/dist/core/ExcelWriter.js +152 -0
- package/dist/core/ExportText.js +28 -0
- package/dist/core/MarkOrigin.js +88 -0
- package/dist/core/MarkStyle.js +294 -0
- package/dist/core/Palette.js +27 -7
- package/dist/core/PdfPage.js +13 -0
- package/dist/core/PdfWriter.js +245 -0
- package/dist/core/Scales.js +52 -2
- package/dist/core/SceneOverlay.js +4 -4
- package/dist/core/TextFit.js +37 -0
- package/dist/core/Theme.js +28 -1
- package/dist/core/enums.js +33 -1
- package/dist/core/pathMorph.js +15 -0
- package/dist/data/SampleData.js +1 -17
- package/dist/data/WorldSample.js +23 -0
- package/dist/index.cjs +810 -691
- package/dist/index.d.cts +8390 -2925
- package/dist/index.d.ts +8392 -2927
- package/dist/index.js +106 -56
- package/dist/react/ChartView.js +18 -8
- package/dist/react/GraphHandle.js +2 -0
- package/dist/react/components.js +74 -116
- package/package.json +13 -3
- package/dist/catalog-4UP2YFNR.cjs +0 -7
- package/dist/charts/bars/BulletBar.js +0 -184
- package/dist/charts/bars/ErrorBarColumn.js +0 -147
- package/dist/charts/bars/HistogramBar.js +0 -217
- package/dist/charts/bars/MarimekkoBar.js +0 -191
- package/dist/charts/bars/ParetoBar.js +0 -143
- package/dist/charts/comparison/PanelChart.js +0 -70
- package/dist/charts/comparison/SmallMultipleBars.js +0 -134
- package/dist/charts/comparison/SpanChart.js +0 -118
- package/dist/charts/comparison/TrellisChart.js +0 -158
- package/dist/charts/distribution/RidgeSlabs3DChart.js +0 -169
- package/dist/charts/financial/Candlestick3D.js +0 -157
- package/dist/charts/flow/BumpFlow.js +0 -212
- package/dist/charts/flow/StreamFlow.js +0 -174
- package/dist/charts/geomaps/ThiessenMap.js +0 -106
- package/dist/charts/hierarchy/ConeTree3D.js +0 -135
- package/dist/charts/hierarchy/Hierarchy3DChart.js +0 -27
- package/dist/charts/hierarchy/SpherePack3D.js +0 -81
- package/dist/charts/hierarchy/SunburstTower3D.js +0 -93
- package/dist/charts/hierarchy/TreemapSkyline3D.js +0 -98
- package/dist/charts/linesareas/ConnectedScatterTime.js +0 -206
- package/dist/charts/linesareas/SmallMultipleLines.js +0 -153
- package/dist/charts/mathematical/ParametricSurface3D.js +0 -234
- package/dist/charts/mathematical/StrangeAttractor3D.js +0 -251
- package/dist/charts/proportion/ConeFunnel.js +0 -126
- package/dist/charts/proportion/ExtrudedPieChart.js +0 -123
- package/dist/charts/scatter/BeeswarmScatter.js +0 -151
- package/dist/charts/scatter/BinnedScatterBase.js +0 -51
- package/dist/charts/scatter/ContourScatter.js +0 -116
- package/dist/charts/scatter/GridBinScatter.js +0 -98
- package/dist/charts/scatter/HexbinScatter.js +0 -74
- package/dist/charts/scatter/StripScatter.js +0 -107
- package/dist/charts/scatter/SunflowerScatter.js +0 -109
- package/dist/charts/signals/ConstellationChart.js +0 -153
- package/dist/charts/signals/CorrelogramChart.js +0 -171
- package/dist/charts/signals/EyeDiagramChart.js +0 -155
- package/dist/charts/signals/PoleZeroChart.js +0 -164
- package/dist/charts/signals/SpectrogramChart.js +0 -79
- package/dist/charts/timeline/CalendarSkylineChart.js +0 -128
- package/dist/charts/timeline/ScheduleBlocksChart.js +0 -119
- package/dist/charts/timeline/SpiralTimeline.js +0 -154
- package/dist/charts/timeline/StreamTimeline.js +0 -141
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { ChartFamily, ColorScale, DataShape, Dimension } from "../../core/enums.js";
|
|
2
|
+
import { ContourLabels } from "../density/ContourLabels.js";
|
|
3
|
+
import { FieldInterpolation } from "../density/DensityEnums.js";
|
|
4
|
+
import { FieldChart } from "../density/FieldChart.js";
|
|
5
|
+
import { FieldContours } from "../density/FieldContours.js";
|
|
6
|
+
import { Draft } from "../scientific/Draft.js";
|
|
7
|
+
import { HovmollerGuides, HovmollerLongitudes, HovmollerReference, HovmollerSection, HovmollerStyle, HovmollerTimeFlow } from "./HovmollerEnums.js";
|
|
8
|
+
import { HovmollerField } from "./HovmollerField.js";
|
|
9
|
+
import { HovmollerSample } from "./HovmollerSample.js";
|
|
10
|
+
import { HovmollerSpeed } from "./HovmollerSpeed.js";
|
|
11
|
+
import { bisectCenter, bisectRight, extent, pointer, range, scaleLinear, scaleTime, tickStep, ticks, timeFormat } from "d3";
|
|
12
|
+
// src/charts/earthscience/HovmollerDiagram.ts
|
|
13
|
+
var STEPS = [
|
|
14
|
+
.1,
|
|
15
|
+
.2,
|
|
16
|
+
.5,
|
|
17
|
+
1,
|
|
18
|
+
2,
|
|
19
|
+
5,
|
|
20
|
+
10,
|
|
21
|
+
15,
|
|
22
|
+
20,
|
|
23
|
+
30,
|
|
24
|
+
45,
|
|
25
|
+
60,
|
|
26
|
+
90,
|
|
27
|
+
120,
|
|
28
|
+
180
|
|
29
|
+
];
|
|
30
|
+
var MOST_LEVELS = 60;
|
|
31
|
+
var SPOTS = [
|
|
32
|
+
.86,
|
|
33
|
+
.14,
|
|
34
|
+
.68,
|
|
35
|
+
.32,
|
|
36
|
+
.5
|
|
37
|
+
];
|
|
38
|
+
var CHIPS = [
|
|
39
|
+
0,
|
|
40
|
+
.25,
|
|
41
|
+
.5,
|
|
42
|
+
.75,
|
|
43
|
+
1
|
|
44
|
+
];
|
|
45
|
+
var DAY = 864e5;
|
|
46
|
+
/** Guide labels sit this far above their line, so its dashes do not strike through the text. */
|
|
47
|
+
var LIFT = 6;
|
|
48
|
+
var LINE_FLOOR = .35;
|
|
49
|
+
var stamp = timeFormat("%b %-d");
|
|
50
|
+
var full = timeFormat("%b %-d, %Y");
|
|
51
|
+
var degrees = (value) => value.toLocaleString("en", { maximumFractionDigits: 1 });
|
|
52
|
+
var percent = (share) => `${Math.round(share * 100)}%`;
|
|
53
|
+
/**
|
|
54
|
+
* Hovmöller diagram (Hovmöller 1949, "The trough-and-ridge diagram", Tellus 1): a field averaged over one space
|
|
55
|
+
* coordinate and drawn as position against time, so moving features appear as slanted bands whose slope is their
|
|
56
|
+
* speed. Rows averaged over latitude are cos φ weighted, global sections get a cyclic column, and negative contours are dashed.
|
|
57
|
+
* Use it to trace propagation: troughs and ridges, Madden-Julian and equatorial waves, or SST and sea level anomalies.
|
|
58
|
+
*/
|
|
59
|
+
var HovmollerDiagram = class HovmollerDiagram extends FieldChart {
|
|
60
|
+
static meta = {
|
|
61
|
+
id: "hovmoller-diagram",
|
|
62
|
+
name: "Hovmöller diagram",
|
|
63
|
+
family: ChartFamily.Scientific,
|
|
64
|
+
dimension: Dimension.Two,
|
|
65
|
+
shape: DataShape.Table,
|
|
66
|
+
description: "Maps a field against time and longitude or latitude, so travelling waves appear as slanted bands whose slope gives their speed.",
|
|
67
|
+
tags: [
|
|
68
|
+
"time-longitude",
|
|
69
|
+
"trough and ridge",
|
|
70
|
+
"propagation",
|
|
71
|
+
"phase speed",
|
|
72
|
+
"Madden-Julian",
|
|
73
|
+
"radon transform",
|
|
74
|
+
"anomaly"
|
|
75
|
+
],
|
|
76
|
+
sample: (seed) => HovmollerSample.tropics(seed),
|
|
77
|
+
variants: {
|
|
78
|
+
section: Object.values(HovmollerSection),
|
|
79
|
+
style: Object.values(HovmollerStyle),
|
|
80
|
+
field: [ColorScale.Diverging, ColorScale.Sequential],
|
|
81
|
+
reference: Object.values(HovmollerReference),
|
|
82
|
+
guides: Object.values(HovmollerGuides),
|
|
83
|
+
timeFlow: Object.values(HovmollerTimeFlow),
|
|
84
|
+
longitudes: [HovmollerLongitudes.Given, HovmollerLongitudes.Greenwich]
|
|
85
|
+
},
|
|
86
|
+
valid: ({ section, field, reference, timeFlow, longitudes }) => field === ColorScale.Diverging === (reference !== HovmollerReference.None) && (section === HovmollerSection.Longitude || timeFlow === HovmollerTimeFlow.Down && longitudes === HovmollerLongitudes.Given)
|
|
87
|
+
};
|
|
88
|
+
#cache = null;
|
|
89
|
+
#probe = null;
|
|
90
|
+
#position = null;
|
|
91
|
+
defaults() {
|
|
92
|
+
return {
|
|
93
|
+
time: "time",
|
|
94
|
+
longitude: "longitude",
|
|
95
|
+
latitude: "latitude",
|
|
96
|
+
value: "value",
|
|
97
|
+
section: HovmollerSection.Longitude,
|
|
98
|
+
style: HovmollerStyle.Filled,
|
|
99
|
+
field: ColorScale.Diverging,
|
|
100
|
+
reference: HovmollerReference.TimeMean,
|
|
101
|
+
guides: HovmollerGuides.Speeds,
|
|
102
|
+
timeFlow: HovmollerTimeFlow.Down,
|
|
103
|
+
longitudes: HovmollerLongitudes.Given,
|
|
104
|
+
levels: 10,
|
|
105
|
+
labels: false,
|
|
106
|
+
daysPerUnit: 1
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
get liveMotion() {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
get along() {
|
|
113
|
+
return this.options.section === HovmollerSection.Longitude;
|
|
114
|
+
}
|
|
115
|
+
get diverging() {
|
|
116
|
+
return this.options.field === ColorScale.Diverging;
|
|
117
|
+
}
|
|
118
|
+
/** Gridding and fitting are slow and a zoom repaints every frame, so the result is kept until its inputs change. */
|
|
119
|
+
reading() {
|
|
120
|
+
const { data, time, longitude, latitude, value, section, band, longitudes, reference, field, levels, interval, center, daysPerUnit, guideLatitude } = this.options;
|
|
121
|
+
const key = JSON.stringify([
|
|
122
|
+
time,
|
|
123
|
+
longitude,
|
|
124
|
+
latitude,
|
|
125
|
+
value,
|
|
126
|
+
section,
|
|
127
|
+
band,
|
|
128
|
+
longitudes,
|
|
129
|
+
reference,
|
|
130
|
+
field,
|
|
131
|
+
levels,
|
|
132
|
+
interval,
|
|
133
|
+
center,
|
|
134
|
+
daysPerUnit,
|
|
135
|
+
guideLatitude
|
|
136
|
+
]);
|
|
137
|
+
if (this.#cache?.data === data && this.#cache.key === key && !this.liveFrame) return this.#cache.reading;
|
|
138
|
+
const read = {
|
|
139
|
+
time,
|
|
140
|
+
longitude,
|
|
141
|
+
latitude,
|
|
142
|
+
value,
|
|
143
|
+
section,
|
|
144
|
+
band,
|
|
145
|
+
longitudes,
|
|
146
|
+
daysPerUnit
|
|
147
|
+
};
|
|
148
|
+
const base = HovmollerField.read(data, read), shown = base.anomaly(reference);
|
|
149
|
+
const [low = 0, high = 0] = extent(shown.values.filter((v) => !Number.isNaN(v)));
|
|
150
|
+
const middle = this.diverging ? center ?? (low < 0 && high > 0 ? 0 : (low + high) / 2) : low;
|
|
151
|
+
const reach = this.diverging ? Math.max(Math.abs(low - middle), Math.abs(high - middle)) : high - low;
|
|
152
|
+
const reading = {
|
|
153
|
+
base,
|
|
154
|
+
field: shown.closed(),
|
|
155
|
+
levels: this.contourLevels(low, high, middle, reach),
|
|
156
|
+
low,
|
|
157
|
+
high,
|
|
158
|
+
middle,
|
|
159
|
+
reach
|
|
160
|
+
};
|
|
161
|
+
this.#cache = {
|
|
162
|
+
data,
|
|
163
|
+
key,
|
|
164
|
+
reading
|
|
165
|
+
};
|
|
166
|
+
return reading;
|
|
167
|
+
}
|
|
168
|
+
/** Round levels: mirrored about the middle on a diverging field, so the two signs are drawn alike. */
|
|
169
|
+
contourLevels(low, high, middle, reach) {
|
|
170
|
+
const { levels, interval } = this.options;
|
|
171
|
+
if (!(reach > 0)) return [];
|
|
172
|
+
const asked = Math.max(1, Math.round(levels));
|
|
173
|
+
const allowed = interval && interval > 0 && reach / interval <= MOST_LEVELS ? interval : null;
|
|
174
|
+
if (!this.diverging) return (allowed ? range(Math.ceil(low / allowed) * allowed, high, allowed) : ticks(low, high, asked)).filter((value) => value > low && value < high);
|
|
175
|
+
const step = allowed ?? tickStep(0, reach, Math.max(1, Math.floor(asked / 2)));
|
|
176
|
+
const arm = range(step, reach, step);
|
|
177
|
+
return [
|
|
178
|
+
...arm.map((value) => middle - value).reverse(),
|
|
179
|
+
middle,
|
|
180
|
+
...arm.map((value) => middle + value)
|
|
181
|
+
].filter((value) => value > low && value < high);
|
|
182
|
+
}
|
|
183
|
+
scales(ctx) {
|
|
184
|
+
this.clearRamp();
|
|
185
|
+
const { width, height } = ctx.plot;
|
|
186
|
+
const { field } = this.reading();
|
|
187
|
+
const moments = field.times, places = field.positions;
|
|
188
|
+
const wide = field.dated ? DAY : 1;
|
|
189
|
+
const [from = 0, to = 1] = extent(moments), [start = this.along ? 0 : -90, stop = this.along ? 360 : 90] = extent(places);
|
|
190
|
+
const span = from === to ? [from - wide, to + wide] : [from, to];
|
|
191
|
+
const reach = start === stop ? [start - 1, stop + 1] : [start, stop];
|
|
192
|
+
const time = field.dated ? scaleTime().domain(span) : scaleLinear().domain(span);
|
|
193
|
+
const position = this.#position = scaleLinear().domain(reach);
|
|
194
|
+
position.ticks = (count = 6) => {
|
|
195
|
+
const [a, b] = [...position.domain()].sort((p, q) => p - q);
|
|
196
|
+
const step = STEPS.find((candidate) => (b - a) / candidate <= count) ?? 180;
|
|
197
|
+
return range(Math.ceil(a / step) * step, b + step * 1e-6, step);
|
|
198
|
+
};
|
|
199
|
+
if (!this.along) {
|
|
200
|
+
time.range([0, width]);
|
|
201
|
+
position.range([height, 0]);
|
|
202
|
+
return {
|
|
203
|
+
x: time,
|
|
204
|
+
y: position,
|
|
205
|
+
position,
|
|
206
|
+
time
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
position.range([0, width]);
|
|
210
|
+
time.range([height, 0]);
|
|
211
|
+
if (this.options.timeFlow === HovmollerTimeFlow.Down) time.domain([...time.domain()].reverse());
|
|
212
|
+
return {
|
|
213
|
+
x: position,
|
|
214
|
+
y: time,
|
|
215
|
+
position,
|
|
216
|
+
time
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
tickFormat(scale) {
|
|
220
|
+
if (scale === this.#position) return (value) => this.placeName(+value);
|
|
221
|
+
return super.tickFormat(scale) ?? (this.reading().field.dated ? (value) => stamp(value) : null);
|
|
222
|
+
}
|
|
223
|
+
placeName(value) {
|
|
224
|
+
if (!this.along) return value === 0 ? "Eq" : `${degrees(Math.abs(value))}°${value > 0 ? "N" : "S"}`;
|
|
225
|
+
const turned = ((value + 180) % 360 + 360) % 360 - 180;
|
|
226
|
+
return turned === 0 || turned === -180 ? `${degrees(Math.abs(turned))}°` : `${degrees(Math.abs(turned))}°${turned > 0 ? "E" : "W"}`;
|
|
227
|
+
}
|
|
228
|
+
momentName(time) {
|
|
229
|
+
return this.reading().field.dated ? full(new Date(time)) : `${degrees(time)}`;
|
|
230
|
+
}
|
|
231
|
+
/** Fractional grid indexes to pixels, keeping stretched grids true. */
|
|
232
|
+
projector({ position, time }, field) {
|
|
233
|
+
const across = scaleLinear(range(field.columns), field.positions), down = scaleLinear(range(field.rows), field.times);
|
|
234
|
+
return (i, j) => this.pixel({
|
|
235
|
+
position,
|
|
236
|
+
time
|
|
237
|
+
}, across(i), down(j));
|
|
238
|
+
}
|
|
239
|
+
pixel({ position, time }, place, moment) {
|
|
240
|
+
const a = position(place), b = time(moment);
|
|
241
|
+
return this.along ? [a, b] : [b, a];
|
|
242
|
+
}
|
|
243
|
+
tone(ctx, { middle, reach, low }, value) {
|
|
244
|
+
if (!(reach > 0)) return this.diverging ? ctx.palette.diverging(0) : ctx.palette.sequential(.5);
|
|
245
|
+
return this.diverging ? ctx.palette.diverging((value - middle) / reach) : ctx.palette.sequential((value - low) / reach);
|
|
246
|
+
}
|
|
247
|
+
bands(ctx, reading) {
|
|
248
|
+
const stops = [
|
|
249
|
+
reading.low,
|
|
250
|
+
...reading.levels,
|
|
251
|
+
reading.high
|
|
252
|
+
];
|
|
253
|
+
return stops.slice(0, -1).map((from, i) => ({
|
|
254
|
+
from,
|
|
255
|
+
to: stops[i + 1],
|
|
256
|
+
fill: this.tone(ctx, reading, (from + stops[i + 1]) / 2)
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
plotMarks(ctx, axes) {
|
|
260
|
+
const reading = this.reading(), { field, levels } = reading;
|
|
261
|
+
const { style, labels } = this.options;
|
|
262
|
+
const count = field.values.length;
|
|
263
|
+
const contours = field.columns > 1 && field.rows > 1 && count ? new FieldContours(Array.from(field.values), field.columns, field.rows, this.projector(axes, field), FieldInterpolation.Smooth) : null;
|
|
264
|
+
const bands = count ? this.bands(ctx, reading) : [];
|
|
265
|
+
const cells = count > 0 && (style === HovmollerStyle.Cells || !contours);
|
|
266
|
+
const filled = !!contours && style === HovmollerStyle.Filled;
|
|
267
|
+
const lines = style === HovmollerStyle.Lines;
|
|
268
|
+
const shades = cells ? this.cells(axes, field, bands, levels) : [];
|
|
269
|
+
const areas = filled ? bands.map((band, i) => ({
|
|
270
|
+
band,
|
|
271
|
+
d: contours.at(i ? band.from : reading.low - 1 - reading.reach).area
|
|
272
|
+
})) : [];
|
|
273
|
+
this.fills(ctx, axes, "hov-bands", areas, reading);
|
|
274
|
+
this.fills(ctx, axes, "hov-cells", shades, reading);
|
|
275
|
+
const drawn = (contours ? levels.map((value) => ({
|
|
276
|
+
value,
|
|
277
|
+
label: FieldChart.short(value),
|
|
278
|
+
shape: contours.at(value)
|
|
279
|
+
})) : []).filter((level) => level.shape.line);
|
|
280
|
+
const paths = ctx.marks("hov-lines", "path", drawn, (d) => d.value).attr("fill", "none").attr("stroke-linejoin", "round").attr("stroke-linecap", "round").attr("stroke", (d) => lines ? this.lineTone(ctx, reading, d.value) : ctx.theme.ink).attr("stroke-width", lines ? 2 : 1).attr("stroke-opacity", lines ? 1 : .45).attr("stroke-dasharray", (d) => this.diverging && d.value < reading.middle ? "5 3" : null);
|
|
281
|
+
this.reveal(ctx, paths, { d: (d) => d.shape.line }, 20);
|
|
282
|
+
if (lines) ctx.hover(paths, (d) => ({
|
|
283
|
+
rows: [[this.valueName, d.label]],
|
|
284
|
+
color: this.lineTone(ctx, reading, d.value)
|
|
285
|
+
}));
|
|
286
|
+
else paths.style("pointer-events", "none");
|
|
287
|
+
ContourLabels.draw(ctx, labels ? drawn.map((d) => ({
|
|
288
|
+
text: d.label,
|
|
289
|
+
polylines: d.shape.polylines
|
|
290
|
+
})) : [], "hov-labels");
|
|
291
|
+
this.drawGuides(ctx, axes, reading);
|
|
292
|
+
if (count) this.showScale(ctx, reading);
|
|
293
|
+
}
|
|
294
|
+
lineTone(ctx, reading, value) {
|
|
295
|
+
const { middle, reach, low } = reading;
|
|
296
|
+
if (!(reach > 0)) return ctx.theme.inkSecondary;
|
|
297
|
+
if (!this.diverging) return ctx.palette.sequential(LINE_FLOOR + .65 * ((value - low) / reach));
|
|
298
|
+
const t = (value - middle) / reach;
|
|
299
|
+
return t === 0 ? ctx.theme.inkMuted : ctx.palette.diverging(Math.sign(t) * (LINE_FLOOR + .65 * Math.abs(t)));
|
|
300
|
+
}
|
|
301
|
+
get valueName() {
|
|
302
|
+
return this.options.valueLabel ?? "Value";
|
|
303
|
+
}
|
|
304
|
+
showScale(ctx, reading) {
|
|
305
|
+
const { middle, reach, low } = reading;
|
|
306
|
+
if (!(reach > 0)) return;
|
|
307
|
+
const at = (t) => this.diverging ? middle + (2 * t - 1) * reach : low + t * reach;
|
|
308
|
+
this.showKey(CHIPS.map((t) => ({
|
|
309
|
+
key: `ramp-${t}`,
|
|
310
|
+
label: FieldChart.short(at(t)),
|
|
311
|
+
color: this.tone(ctx, reading, at(t))
|
|
312
|
+
})));
|
|
313
|
+
}
|
|
314
|
+
fills(ctx, axes, name, shapes, reading) {
|
|
315
|
+
const paths = ctx.marks(name, "path", shapes, (d) => d.band.from).attr("fill", (d) => d.band.fill);
|
|
316
|
+
this.reveal(ctx, paths, { d: (d) => d.d }, 12);
|
|
317
|
+
paths.on("pointermove.probe", (event) => {
|
|
318
|
+
this.#probe = pointer(event, ctx.root.node());
|
|
319
|
+
}).on("pointerleave.probe", () => {
|
|
320
|
+
this.#probe = null;
|
|
321
|
+
});
|
|
322
|
+
ctx.hover(paths, ({ band }) => this.tooltip(axes, reading, band));
|
|
323
|
+
}
|
|
324
|
+
tooltip({ position, time }, { field }, band) {
|
|
325
|
+
const within = {
|
|
326
|
+
color: band.fill,
|
|
327
|
+
rows: [[this.valueName, `${FieldChart.short(band.from)} to ${FieldChart.short(band.to)}`]]
|
|
328
|
+
};
|
|
329
|
+
if (!this.#probe) return within;
|
|
330
|
+
const [px, py] = this.#probe;
|
|
331
|
+
const place = position.invert(this.along ? px : py), moment = +time.invert(this.along ? py : px);
|
|
332
|
+
const i = bisectCenter(field.positions, place), j = bisectCenter(field.times, moment);
|
|
333
|
+
const value = field.values[i + j * field.columns];
|
|
334
|
+
return {
|
|
335
|
+
title: this.momentName(field.times[j]),
|
|
336
|
+
color: band.fill,
|
|
337
|
+
rows: [[this.along ? "Longitude" : "Latitude", this.placeName(field.positions[i])], [this.valueName, Number.isNaN(value) ? "no data" : FieldChart.short(value)]]
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** One path per shade, with runs of equal shade merged along the rows of the grid. Gaps stay unpainted. */
|
|
341
|
+
cells(axes, field, bands, levels) {
|
|
342
|
+
const { columns, rows, positions, times, values } = field;
|
|
343
|
+
const edges = (centers, [low, high]) => [
|
|
344
|
+
low,
|
|
345
|
+
...centers.slice(1).map((value, i) => (value + centers[i]) / 2),
|
|
346
|
+
high
|
|
347
|
+
];
|
|
348
|
+
const across = edges(positions, positions.length > 1 ? [positions[0], positions.at(-1)] : axes.position.domain());
|
|
349
|
+
const down = edges(times, times.length > 1 ? [times[0], times.at(-1)] : axes.time.domain().map(Number).sort((a, b) => a - b));
|
|
350
|
+
const shade = (k) => Number.isNaN(values[k]) ? -1 : bisectRight(levels, values[k]);
|
|
351
|
+
const paths = bands.map(() => []);
|
|
352
|
+
for (let j = 0; j < rows; j++) {
|
|
353
|
+
let i = 0;
|
|
354
|
+
while (i < columns) {
|
|
355
|
+
const tone = shade(i + j * columns);
|
|
356
|
+
let end = i + 1;
|
|
357
|
+
while (end < columns && shade(end + j * columns) === tone) end++;
|
|
358
|
+
if (tone >= 0) {
|
|
359
|
+
const [x0, y0] = this.pixel(axes, across[i], down[j]), [x1, y1] = this.pixel(axes, across[end], down[j + 1]);
|
|
360
|
+
paths[tone].push(`M${Math.round(x0)} ${Math.round(y0)}H${Math.round(x1)}V${Math.round(y1)}H${Math.round(x0)}Z`);
|
|
361
|
+
}
|
|
362
|
+
i = end;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return bands.map((band, i) => ({
|
|
366
|
+
band,
|
|
367
|
+
d: paths[i].join("")
|
|
368
|
+
})).filter((shape) => shape.d);
|
|
369
|
+
}
|
|
370
|
+
/** Stated speeds spread evenly across space at mid-record, or fitted ones laid along the strongest ridge or trough. */
|
|
371
|
+
guides(reading) {
|
|
372
|
+
const { base } = reading;
|
|
373
|
+
const { guides, guideSpeeds, guideLatitude, band } = this.options;
|
|
374
|
+
if (guides === HovmollerGuides.None || base.columns < 2 || base.rows < 2) return [];
|
|
375
|
+
const latitude = this.along ? guideLatitude ?? (band ? (band[0] + band[1]) / 2 : 0) : 0;
|
|
376
|
+
if (guides === HovmollerGuides.Fitted) return reading.fits ??= [1, -1].flatMap((direction) => {
|
|
377
|
+
const fit = HovmollerSpeed.fit(base, direction, latitude);
|
|
378
|
+
return fit ? [{
|
|
379
|
+
key: `fit-${direction}`,
|
|
380
|
+
rate: fit.rate,
|
|
381
|
+
anchor: fit.origin,
|
|
382
|
+
day: 0,
|
|
383
|
+
speed: fit.speed,
|
|
384
|
+
share: fit.share
|
|
385
|
+
}] : [];
|
|
386
|
+
});
|
|
387
|
+
const speeds = guideSpeeds ?? (this.along ? [5, -5] : [1.5, -1.5]);
|
|
388
|
+
const low = base.positions[0], span = base.positions.at(-1) - low, day = base.days.at(-1) / 2;
|
|
389
|
+
return speeds.filter(Number.isFinite).map((speed, i) => ({
|
|
390
|
+
key: `speed-${i}`,
|
|
391
|
+
rate: HovmollerSpeed.degreesPerDay(speed, latitude),
|
|
392
|
+
day,
|
|
393
|
+
speed,
|
|
394
|
+
anchor: low + span * (i + 1) / (speeds.length + 1)
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
guideLines(axes, { base }, guide, width, height) {
|
|
398
|
+
const stop = base.days.at(-1);
|
|
399
|
+
const copies = base.cyclic ? Math.ceil(Math.abs(guide.rate) * stop / 360) + 1 : 0;
|
|
400
|
+
return range(-copies, copies + 1).flatMap((copy) => {
|
|
401
|
+
const end = (day) => this.pixel(axes, guide.anchor + copy * 360 + guide.rate * (day - guide.day), base.timeAt(day));
|
|
402
|
+
const ends = HovmollerDiagram.clip(end(0), end(stop), width, height);
|
|
403
|
+
return ends ? [{
|
|
404
|
+
guide,
|
|
405
|
+
ends,
|
|
406
|
+
d: `M${ends[0].map((v) => v.toFixed(1)).join(",")}L${ends[1].map((v) => v.toFixed(1)).join(",")}`
|
|
407
|
+
}] : [];
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/** Liang-Barsky clipping of a segment to the plot. */
|
|
411
|
+
static clip([x0, y0], [x1, y1], width, height) {
|
|
412
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
413
|
+
let enter = 0, leave = 1;
|
|
414
|
+
for (const [p, q] of [
|
|
415
|
+
[-dx, x0],
|
|
416
|
+
[dx, width - x0],
|
|
417
|
+
[-dy, y0],
|
|
418
|
+
[dy, height - y0]
|
|
419
|
+
]) {
|
|
420
|
+
if (p === 0) {
|
|
421
|
+
if (q < 0) return null;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const t = q / p;
|
|
425
|
+
if (p < 0) enter = Math.max(enter, t);
|
|
426
|
+
else leave = Math.min(leave, t);
|
|
427
|
+
}
|
|
428
|
+
if (enter >= leave) return null;
|
|
429
|
+
return [[x0 + enter * dx, y0 + enter * dy], [x0 + leave * dx, y0 + leave * dy]];
|
|
430
|
+
}
|
|
431
|
+
drawGuides(ctx, axes, reading) {
|
|
432
|
+
const { width, height } = ctx.plot;
|
|
433
|
+
const lines = this.guides(reading).flatMap((guide) => this.guideLines(axes, reading, guide, width, height));
|
|
434
|
+
const paths = ctx.marks("hov-guides", "path", lines, (_, i) => i).attr("fill", "none").attr("stroke", ctx.theme.ink).attr("stroke-width", 2).attr("stroke-dasharray", "8 4").attr("stroke-linecap", "round");
|
|
435
|
+
ctx.set(paths, { d: (d) => d.d }, { d: (d) => d.d });
|
|
436
|
+
ctx.hover(paths, ({ guide }) => ({
|
|
437
|
+
title: this.guideName(guide),
|
|
438
|
+
rows: [["Slope", `${degrees(Math.abs(guide.rate))}°/day`], ...guide.share === void 0 ? [] : [["Variance explained", percent(guide.share)]]]
|
|
439
|
+
}));
|
|
440
|
+
const placed = [];
|
|
441
|
+
for (const key of new Set(lines.map((line) => line.guide.key))) {
|
|
442
|
+
const longest = lines.filter((line) => line.guide.key === key).reduce((a, b) => Math.hypot(b.ends[1][0] - b.ends[0][0], b.ends[1][1] - b.ends[0][1]) > Math.hypot(a.ends[1][0] - a.ends[0][0], a.ends[1][1] - a.ends[0][1]) ? b : a);
|
|
443
|
+
const text = this.guideName(longest.guide);
|
|
444
|
+
const spot = SPOTS.map((t) => ({
|
|
445
|
+
text,
|
|
446
|
+
size: 10.5,
|
|
447
|
+
tone: "ink",
|
|
448
|
+
x: longest.ends[0][0] + t * (longest.ends[1][0] - longest.ends[0][0]),
|
|
449
|
+
y: longest.ends[0][1] + t * (longest.ends[1][1] - longest.ends[0][1]) - LIFT
|
|
450
|
+
})).find((caption) => Draft.declutter([...placed, caption], width, height).length === placed.length + 1);
|
|
451
|
+
if (spot) placed.push(spot);
|
|
452
|
+
}
|
|
453
|
+
Draft.captions(ctx, "hov-guide-labels", placed).attr("paint-order", "stroke").attr("stroke", ctx.theme.surface).attr("stroke-width", 4).attr("stroke-linejoin", "round");
|
|
454
|
+
}
|
|
455
|
+
guideName({ speed, share }) {
|
|
456
|
+
const heading = this.along ? speed >= 0 ? "E" : "W" : speed >= 0 ? "N" : "S";
|
|
457
|
+
return `${share === void 0 ? "" : "fit "}${degrees(Math.abs(speed))} m/s ${heading}`;
|
|
458
|
+
}
|
|
459
|
+
table() {
|
|
460
|
+
const { data } = this.options;
|
|
461
|
+
if (Array.isArray(data) || !data) return super.table();
|
|
462
|
+
const { x = [], y = [], z = [] } = data;
|
|
463
|
+
return {
|
|
464
|
+
columns: [
|
|
465
|
+
"time",
|
|
466
|
+
"position",
|
|
467
|
+
"value"
|
|
468
|
+
],
|
|
469
|
+
rows: y.flatMap((time, t) => x.map((position, p) => [
|
|
470
|
+
time,
|
|
471
|
+
position,
|
|
472
|
+
z[t]?.[p] ?? null
|
|
473
|
+
]))
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
export { HovmollerDiagram };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/charts/earthscience/HovmollerEnums.ts
|
|
2
|
+
/** Which coordinate runs against time: longitude across with time down, or time across with latitude up. */
|
|
3
|
+
var HovmollerSection = /* @__PURE__ */ function(HovmollerSection) {
|
|
4
|
+
HovmollerSection["Longitude"] = "longitude";
|
|
5
|
+
HovmollerSection["Latitude"] = "latitude";
|
|
6
|
+
return HovmollerSection;
|
|
7
|
+
}({});
|
|
8
|
+
/** Filled bands between contours, the grid's own cells under contours, or contours alone. */
|
|
9
|
+
var HovmollerStyle = /* @__PURE__ */ function(HovmollerStyle) {
|
|
10
|
+
HovmollerStyle["Filled"] = "filled";
|
|
11
|
+
HovmollerStyle["Cells"] = "cells";
|
|
12
|
+
HovmollerStyle["Lines"] = "lines";
|
|
13
|
+
return HovmollerStyle;
|
|
14
|
+
}({});
|
|
15
|
+
/** What is taken away before plotting: each place's mean over time, each moment's mean over space, or nothing. */
|
|
16
|
+
var HovmollerReference = /* @__PURE__ */ function(HovmollerReference) {
|
|
17
|
+
HovmollerReference["TimeMean"] = "time-mean";
|
|
18
|
+
HovmollerReference["SpaceMean"] = "space-mean";
|
|
19
|
+
HovmollerReference["None"] = "none";
|
|
20
|
+
return HovmollerReference;
|
|
21
|
+
}({});
|
|
22
|
+
/** Lines of constant phase speed: stated speeds, none, or speeds fitted to the field. */
|
|
23
|
+
var HovmollerGuides = /* @__PURE__ */ function(HovmollerGuides) {
|
|
24
|
+
HovmollerGuides["Speeds"] = "speeds";
|
|
25
|
+
HovmollerGuides["None"] = "none";
|
|
26
|
+
HovmollerGuides["Fitted"] = "fitted";
|
|
27
|
+
return HovmollerGuides;
|
|
28
|
+
}({});
|
|
29
|
+
/** Hovmöller's own layout runs time downward. */
|
|
30
|
+
var HovmollerTimeFlow = /* @__PURE__ */ function(HovmollerTimeFlow) {
|
|
31
|
+
HovmollerTimeFlow["Down"] = "down";
|
|
32
|
+
HovmollerTimeFlow["Up"] = "up";
|
|
33
|
+
return HovmollerTimeFlow;
|
|
34
|
+
}({});
|
|
35
|
+
/** Longitudes as supplied, or wrapped to −180..180 or 0..360. */
|
|
36
|
+
var HovmollerLongitudes = /* @__PURE__ */ function(HovmollerLongitudes) {
|
|
37
|
+
HovmollerLongitudes["Given"] = "given";
|
|
38
|
+
HovmollerLongitudes["Greenwich"] = "greenwich";
|
|
39
|
+
HovmollerLongitudes["Dateline"] = "dateline";
|
|
40
|
+
return HovmollerLongitudes;
|
|
41
|
+
}({});
|
|
42
|
+
export { HovmollerGuides, HovmollerLongitudes, HovmollerReference, HovmollerSection, HovmollerStyle, HovmollerTimeFlow };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { HovmollerLongitudes, HovmollerReference, HovmollerSection } from "./HovmollerEnums.js";
|
|
2
|
+
import { ascending, median, range } from "d3";
|
|
3
|
+
// src/charts/earthscience/HovmollerField.ts
|
|
4
|
+
var LIMIT = 400;
|
|
5
|
+
var DAY = 864e5;
|
|
6
|
+
var FULL = 360;
|
|
7
|
+
var finite = (cell) => cell === null || cell === void 0 || cell === "" || cell instanceof Date ? null : Number.isFinite(+cell) ? +cell : null;
|
|
8
|
+
var within = ([a, b], value, wraps) => a <= b ? value >= a && value <= b : wraps ? value >= a || value <= b : value >= b && value <= a;
|
|
9
|
+
/** A section gridded for plotting: `values[p + t * columns]`, with NaN where nothing was observed. */
|
|
10
|
+
var HovmollerField = class HovmollerField {
|
|
11
|
+
positions;
|
|
12
|
+
times;
|
|
13
|
+
values;
|
|
14
|
+
section;
|
|
15
|
+
dated;
|
|
16
|
+
daysPerUnit;
|
|
17
|
+
constructor(positions, times, values, section, dated, daysPerUnit) {
|
|
18
|
+
this.positions = positions;
|
|
19
|
+
this.times = times;
|
|
20
|
+
this.values = values;
|
|
21
|
+
this.section = section;
|
|
22
|
+
this.dated = dated;
|
|
23
|
+
this.daysPerUnit = daysPerUnit;
|
|
24
|
+
}
|
|
25
|
+
get columns() {
|
|
26
|
+
return this.positions.length;
|
|
27
|
+
}
|
|
28
|
+
get rows() {
|
|
29
|
+
return this.times.length;
|
|
30
|
+
}
|
|
31
|
+
/** Times in days since the first. */
|
|
32
|
+
get days() {
|
|
33
|
+
const unit = this.dated ? 1 / DAY : this.daysPerUnit;
|
|
34
|
+
return this.times.map((time) => (time - this.times[0]) * unit);
|
|
35
|
+
}
|
|
36
|
+
/** The time `day` days after the first. */
|
|
37
|
+
timeAt(day) {
|
|
38
|
+
return (this.times[0] ?? 0) + day * (this.dated ? DAY : 1 / this.daysPerUnit);
|
|
39
|
+
}
|
|
40
|
+
/** Median distance between positions. */
|
|
41
|
+
get spacing() {
|
|
42
|
+
return median(this.positions.slice(1).map((position, i) => position - this.positions[i])) ?? 0;
|
|
43
|
+
}
|
|
44
|
+
/** The positions run all the way round the globe, so the last is next to the first. */
|
|
45
|
+
get cyclic() {
|
|
46
|
+
if (this.section !== HovmollerSection.Longitude || this.columns < 3) return false;
|
|
47
|
+
return this.positions[0] + FULL - this.positions.at(-1) <= this.spacing * 1.5;
|
|
48
|
+
}
|
|
49
|
+
/** Grids rows or a ready grid. Rows that share a time and position are averaged, by cos φ when longitude sections pool latitudes. */
|
|
50
|
+
static read(data, read) {
|
|
51
|
+
const raw = Array.isArray(data) ? this.fromRows(data, read) : this.fromGrid(data, read);
|
|
52
|
+
const dated = raw.dated;
|
|
53
|
+
const positions = this.slots(raw.readings.map((d) => d.p)), times = this.slots(raw.readings.map((d) => d.t));
|
|
54
|
+
const columns = positions.centers.length, rows = times.centers.length;
|
|
55
|
+
const sum = new Float64Array(columns * rows), weight = new Float64Array(columns * rows);
|
|
56
|
+
for (const { t, p, v, w } of raw.readings) {
|
|
57
|
+
const k = positions.slot(p) + times.slot(t) * columns;
|
|
58
|
+
sum[k] += v * w;
|
|
59
|
+
weight[k] += w;
|
|
60
|
+
}
|
|
61
|
+
const values = sum.map((total, k) => weight[k] > 0 ? total / weight[k] : NaN);
|
|
62
|
+
return new HovmollerField(positions.centers, times.centers, values, read.section, dated, read.daysPerUnit);
|
|
63
|
+
}
|
|
64
|
+
static moment(cell) {
|
|
65
|
+
if (cell instanceof Date) return Number.isFinite(cell.getTime()) ? {
|
|
66
|
+
at: cell.getTime(),
|
|
67
|
+
dated: true
|
|
68
|
+
} : null;
|
|
69
|
+
const number = finite(cell);
|
|
70
|
+
if (number !== null) return {
|
|
71
|
+
at: number,
|
|
72
|
+
dated: false
|
|
73
|
+
};
|
|
74
|
+
const parsed = typeof cell === "string" ? Date.parse(cell) : NaN;
|
|
75
|
+
return Number.isFinite(parsed) ? {
|
|
76
|
+
at: parsed,
|
|
77
|
+
dated: true
|
|
78
|
+
} : null;
|
|
79
|
+
}
|
|
80
|
+
static wrap(longitude, mode) {
|
|
81
|
+
if (mode === HovmollerLongitudes.Given) return longitude;
|
|
82
|
+
const turned = (longitude % FULL + FULL) % FULL;
|
|
83
|
+
return mode === HovmollerLongitudes.Greenwich && turned >= 180 ? turned - FULL : turned;
|
|
84
|
+
}
|
|
85
|
+
static place(position, { section, longitudes }) {
|
|
86
|
+
if (section === HovmollerSection.Longitude) return this.wrap(position, longitudes);
|
|
87
|
+
return Math.abs(position) <= 90 ? position : null;
|
|
88
|
+
}
|
|
89
|
+
static fromRows(rows, read) {
|
|
90
|
+
const along = read.section === HovmollerSection.Longitude;
|
|
91
|
+
const [mine, other] = along ? [read.longitude, read.latitude] : [read.latitude, read.longitude];
|
|
92
|
+
let dated = null;
|
|
93
|
+
return {
|
|
94
|
+
readings: rows.flatMap((row) => {
|
|
95
|
+
if (row === null || typeof row !== "object") return [];
|
|
96
|
+
const moment = this.moment(row[read.time]), raw = finite(row[mine]), v = finite(row[read.value]), pooled = finite(row[other]);
|
|
97
|
+
const p = raw === null ? null : this.place(raw, read);
|
|
98
|
+
if (!moment || p === null || v === null) return [];
|
|
99
|
+
if (pooled !== null && read.band && !within(read.band, pooled, !along)) return [];
|
|
100
|
+
if (along && pooled !== null && Math.abs(pooled) > 90) return [];
|
|
101
|
+
dated ??= moment.dated;
|
|
102
|
+
if (moment.dated !== dated) return [];
|
|
103
|
+
return [{
|
|
104
|
+
t: moment.at,
|
|
105
|
+
p,
|
|
106
|
+
v,
|
|
107
|
+
w: along && pooled !== null ? Math.cos(pooled * Math.PI / 180) : 1
|
|
108
|
+
}];
|
|
109
|
+
}),
|
|
110
|
+
dated: dated ?? false
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
static fromGrid(grid, read) {
|
|
114
|
+
const moments = (grid?.y ?? []).map((cell) => this.moment(cell));
|
|
115
|
+
const dated = moments.find(Boolean)?.dated ?? false;
|
|
116
|
+
return {
|
|
117
|
+
readings: moments.flatMap((moment, j) => {
|
|
118
|
+
if (!moment || moment.dated !== dated) return [];
|
|
119
|
+
return (grid?.x ?? []).flatMap((position, i) => {
|
|
120
|
+
const v = finite(grid?.z[j]?.[i]), raw = finite(position);
|
|
121
|
+
const p = raw === null ? null : this.place(raw, read);
|
|
122
|
+
return v === null || p === null ? [] : [{
|
|
123
|
+
t: moment.at,
|
|
124
|
+
p,
|
|
125
|
+
v,
|
|
126
|
+
w: 1
|
|
127
|
+
}];
|
|
128
|
+
});
|
|
129
|
+
}),
|
|
130
|
+
dated
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** One slot per distinct value, or equal bins once scattered observations would make the grid too large. */
|
|
134
|
+
static slots(values) {
|
|
135
|
+
const centers = [...new Set(values)].sort(ascending);
|
|
136
|
+
if (centers.length <= LIMIT) {
|
|
137
|
+
const index = new Map(centers.map((value, i) => [value, i]));
|
|
138
|
+
return {
|
|
139
|
+
centers,
|
|
140
|
+
slot: (value) => index.get(value)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const low = centers[0], width = (centers.at(-1) - low) / LIMIT;
|
|
144
|
+
return {
|
|
145
|
+
centers: range(LIMIT).map((k) => low + (k + .5) * width),
|
|
146
|
+
slot: (value) => Math.min(399, Math.floor((value - low) / width))
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Removes a mean. Means over latitude are weighted by cos φ, the area of each band. */
|
|
150
|
+
anomaly(reference) {
|
|
151
|
+
if (reference === HovmollerReference.None) return this;
|
|
152
|
+
const { columns, rows, positions } = this;
|
|
153
|
+
const byTime = reference === HovmollerReference.TimeMean;
|
|
154
|
+
const weight = positions.map((position) => this.section === HovmollerSection.Latitude ? Math.cos(position * Math.PI / 180) : 1);
|
|
155
|
+
const sum = new Float64Array(byTime ? columns : rows), mass = new Float64Array(byTime ? columns : rows);
|
|
156
|
+
const key = (k) => byTime ? k % columns : Math.floor(k / columns);
|
|
157
|
+
const area = (k) => byTime ? 1 : weight[k % columns];
|
|
158
|
+
this.values.forEach((value, k) => {
|
|
159
|
+
if (Number.isNaN(value)) return;
|
|
160
|
+
sum[key(k)] += value * area(k);
|
|
161
|
+
mass[key(k)] += area(k);
|
|
162
|
+
});
|
|
163
|
+
return this.with(positions, this.values.map((value, k) => value - sum[key(k)] / mass[key(k)]));
|
|
164
|
+
}
|
|
165
|
+
/** Repeats the first column at +360° so a global field runs right across the plot with no seam. */
|
|
166
|
+
closed() {
|
|
167
|
+
const { columns, rows, positions } = this;
|
|
168
|
+
if (!this.cyclic || positions.at(-1) - positions[0] >= FULL) return this;
|
|
169
|
+
const next = columns + 1, values = new Float64Array(next * rows);
|
|
170
|
+
for (let j = 0; j < rows; j++) {
|
|
171
|
+
values.set(this.values.subarray(j * columns, (j + 1) * columns), j * next);
|
|
172
|
+
values[columns + j * next] = this.values[j * columns];
|
|
173
|
+
}
|
|
174
|
+
return this.with([...positions, positions[0] + FULL], values);
|
|
175
|
+
}
|
|
176
|
+
with(positions, values) {
|
|
177
|
+
return new HovmollerField(positions, this.times, values, this.section, this.dated, this.daysPerUnit);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
export { HovmollerField };
|