@tsingroc/tsingroc-components 3.13.4 → 3.14.1

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.
Files changed (39) hide show
  1. package/dist/components/TsingrocTheme.js +1 -1
  2. package/dist/components/TsingrocTheme.js.map +1 -1
  3. package/dist/deckgl/TiandituLayer.js +6 -17
  4. package/dist/deckgl/TiandituLayer.js.map +1 -1
  5. package/dist/deckgl/WeatherData.d.ts +3 -3
  6. package/dist/echarts/index.js +1 -1
  7. package/dist/echarts/index.js.map +1 -1
  8. package/dist/echarts/series.d.ts +44 -0
  9. package/dist/echarts/series.js +54 -1
  10. package/dist/echarts/series.js.map +1 -1
  11. package/package.json +3 -3
  12. package/src/components/Auth.tsx +389 -0
  13. package/src/components/Calendar.tsx +182 -0
  14. package/src/components/CircularProgress.tsx +38 -0
  15. package/src/components/Header.tsx +136 -0
  16. package/src/components/ImageBackground.tsx +58 -0
  17. package/src/components/IndicatorLight.tsx +106 -0
  18. package/src/components/LineChartEditor.tsx +558 -0
  19. package/src/components/LineChartTable.tsx +285 -0
  20. package/src/components/LinkedLineChart.tsx +223 -0
  21. package/src/components/QuickDateRangePicker.tsx +84 -0
  22. package/src/components/SegmentedButtons.tsx +46 -0
  23. package/src/components/Sidebar.tsx +250 -0
  24. package/src/components/TsingrocDatePicker.tsx +103 -0
  25. package/src/components/TsingrocTheme.tsx +47 -0
  26. package/src/components/UserButton.tsx +152 -0
  27. package/src/components/VerticalColorLegend.tsx +73 -0
  28. package/src/components/WeatherMap.tsx +521 -0
  29. package/src/deckgl/TiandituLayer.ts +56 -0
  30. package/src/deckgl/WeatherData.ts +157 -0
  31. package/src/deckgl/index.ts +4 -0
  32. package/src/echarts/coordinateSystem.ts +132 -0
  33. package/src/echarts/gl.ts +158 -0
  34. package/src/echarts/index.ts +120 -0
  35. package/src/echarts/legend.ts +36 -0
  36. package/src/echarts/radar.ts +46 -0
  37. package/src/echarts/series.ts +441 -0
  38. package/src/echarts/tooltip.ts +17 -0
  39. package/src/index.ts +79 -0
@@ -0,0 +1,521 @@
1
+ import * as wl from "weatherlayers-gl";
2
+ import { Button, type ButtonProps, Flex, Slider, Spin, theme } from "antd";
3
+ import {
4
+ type CSSProperties,
5
+ type HTMLAttributes,
6
+ type ReactNode,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
10
+ import { MdDewPoint, MdThermostat } from "react-icons/md";
11
+ import {
12
+ WiBarometer,
13
+ WiCloudy,
14
+ WiHumidity,
15
+ WiRain,
16
+ WiStrongWind,
17
+ } from "react-icons/wi";
18
+ import { createStyles } from "antd-style";
19
+ import dayjs from "dayjs";
20
+ import DeckGL from "deck.gl";
21
+
22
+ import {
23
+ extractMagnitudeData,
24
+ extractScalarData,
25
+ extractVectorData,
26
+ getBounds,
27
+ type WeatherData,
28
+ } from "../deckgl/WeatherData";
29
+ import { TiandituLayer } from "../deckgl";
30
+ import TsingrocTheme from "./TsingrocTheme";
31
+ import VerticalColorLegend from "./VerticalColorLegend";
32
+
33
+ export { type WeatherData };
34
+
35
+ export interface WeatherMapProps extends HTMLAttributes<HTMLDivElement> {
36
+ /** 天地图平台的 token。*/
37
+ tiandituTk: string;
38
+ /** 要显示的天气数据。如果为 `undefined`,则会显示加载界面。*/
39
+ data: WeatherData | undefined;
40
+ }
41
+
42
+ /**
43
+ * 一张显示各类天气信息的地图。
44
+ *
45
+ * 多余的属性会被传递给组件最外层的 [`div` 元素][1]。
46
+ *
47
+ * [1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
48
+ */
49
+ function WeatherMap(props: WeatherMapProps) {
50
+ const { tiandituTk, data, ...rest } = props;
51
+ const { token } = theme.useToken();
52
+ const boundingBox = data ? getBounds(data) : undefined;
53
+ const [selectedScalar, setSelectedScalar] = useState(SCALAR_VARIABLES[0]);
54
+ const hasWind = !!(
55
+ data &&
56
+ data.data_vars.wind_component_u_10m &&
57
+ data.data_vars.wind_component_v_10m
58
+ );
59
+ const scalarFrames = useMemo(() => {
60
+ if (!data) return undefined;
61
+ if (
62
+ selectedScalar.key === "wind_speed_10m" &&
63
+ !data.data_vars.wind_speed_10m &&
64
+ hasWind
65
+ ) {
66
+ return extractMagnitudeData(
67
+ data,
68
+ "wind_component_u_10m",
69
+ "wind_component_v_10m",
70
+ );
71
+ }
72
+ return extractScalarData(data, selectedScalar.key);
73
+ }, [data, selectedScalar, hasWind]);
74
+ const [showWind, setShowWind] = useState(true);
75
+ const windFrames = useMemo(() => {
76
+ if (!(hasWind && showWind)) return undefined;
77
+ return extractVectorData(
78
+ data,
79
+ "wind_component_u_10m",
80
+ "wind_component_v_10m",
81
+ );
82
+ }, [data, hasWind, showWind]);
83
+ const [timeIdx, setTimeIdx] = useState(0);
84
+ const scalarFrame = scalarFrames?.[timeIdx];
85
+ const windFrame = windFrames?.[timeIdx];
86
+ const palette = useMemo(() => {
87
+ if (!scalarFrame) return undefined;
88
+ return adaptiveColorCoding(
89
+ scalarFrame.min,
90
+ scalarFrame.max,
91
+ selectedScalar.adaptivePalette!,
92
+ );
93
+ }, [selectedScalar, scalarFrame]);
94
+ return (
95
+ <TsingrocTheme>
96
+ <div {...rest} style={{ position: "relative", ...rest.style }}>
97
+ <DeckGL
98
+ initialViewState={{ longitude: 0, latitude: 0, zoom: 0, maxPitch: 0 }}
99
+ controller={{ dragRotate: false }}
100
+ layers={[
101
+ new TiandituLayer({ id: "base", tiandituTk }),
102
+ new TiandituLayer({ id: "ann", tiandituTk, layer: "annotation" }),
103
+ scalarFrame
104
+ ? new wl.RasterLayer({
105
+ id: "scalar",
106
+ opacity: 0.5,
107
+ bounds: boundingBox,
108
+ image: scalarFrame.image,
109
+ palette,
110
+ })
111
+ : null,
112
+ windFrame
113
+ ? new wl.ParticleLayer({
114
+ id: "wind",
115
+ opacity: 0.5,
116
+ bounds: boundingBox,
117
+ image: windFrame,
118
+ numParticles: 1000,
119
+ maxAge: 30,
120
+ speedFactor: 10,
121
+ width: window.devicePixelRatio,
122
+ })
123
+ : null,
124
+ ]}
125
+ />
126
+ {data && (
127
+ <div
128
+ style={{
129
+ position: "absolute",
130
+ inset: "auto 0 0 0",
131
+ padding: "4px 8px 0 8px",
132
+ background: "linear-gradient(to bottom, transparent, #0007)",
133
+ }}
134
+ >
135
+ <Slider
136
+ value={timeIdx}
137
+ min={0}
138
+ max={data.dims.time - 1}
139
+ step={1}
140
+ onChange={setTimeIdx}
141
+ tooltip={{
142
+ formatter: (value) =>
143
+ dayjs
144
+ .unix(data.coords.time[value!])
145
+ .format("YYYY-MM-DD HH:mm:ss"),
146
+ }}
147
+ />
148
+ </div>
149
+ )}
150
+ <Flex
151
+ vertical
152
+ style={{
153
+ position: "absolute",
154
+ inset: "8px 8px auto auto",
155
+ borderRadius: token.borderRadius,
156
+ padding: token.paddingXXS,
157
+ background: token.colorBgMask,
158
+ }}
159
+ >
160
+ {SCALAR_VARIABLES.filter(
161
+ (scalar) =>
162
+ !data ||
163
+ scalar.key in data.data_vars ||
164
+ (scalar.key === "wind_speed_10m" && hasWind),
165
+ ).map((scalar) => (
166
+ <ToolbarButton
167
+ key={scalar.key}
168
+ icon={scalar.icon}
169
+ selected={selectedScalar.key === scalar.key}
170
+ onClick={() => setSelectedScalar(scalar)}
171
+ >
172
+ {scalar.name}
173
+ </ToolbarButton>
174
+ ))}
175
+ <hr style={{ margin: token.marginXXS }} />
176
+ {(!data || hasWind) && (
177
+ <ToolbarButton
178
+ icon={<WiStrongWind />}
179
+ selected={showWind}
180
+ onClick={() => setShowWind((b) => !b)}
181
+ >
182
+ 风力轨迹
183
+ </ToolbarButton>
184
+ )}
185
+ </Flex>
186
+ {palette && (
187
+ <VerticalColorLegend
188
+ title={selectedScalar.name + "(" + selectedScalar.unit + ")"}
189
+ palette={palette}
190
+ style={{
191
+ position: "absolute",
192
+ inset: "auto auto 32px 13px",
193
+ height: 135,
194
+ pointerEvents: "none",
195
+ }}
196
+ />
197
+ )}
198
+ {!data && (
199
+ <Spin
200
+ size="large"
201
+ style={{
202
+ position: "absolute",
203
+ inset: 0,
204
+ background: token.colorFillSecondary,
205
+ display: "flex",
206
+ alignItems: "center",
207
+ justifyContent: "center",
208
+ }}
209
+ />
210
+ )}
211
+ </div>
212
+ </TsingrocTheme>
213
+ );
214
+ }
215
+
216
+ export default WeatherMap;
217
+
218
+ function ToolbarButton(props: ButtonProps & { selected: boolean }) {
219
+ const { selected, className, children, ...rest } = props;
220
+ const { cx, styles } = useStyles();
221
+ return (
222
+ <Button
223
+ color="default"
224
+ variant="text"
225
+ className={cx(
226
+ styles.toolbarButton,
227
+ { [styles.toolbarButtonSelected]: selected },
228
+ className,
229
+ )}
230
+ {...rest}
231
+ >
232
+ <span className={styles.toolbarButtonText}>{children}</span>
233
+ </Button>
234
+ );
235
+ }
236
+
237
+ const useStyles = createStyles(({ token, css, prefixCls }) => {
238
+ return {
239
+ toolbarButton: css`
240
+ padding: 0 ${token.paddingXS}px;
241
+ background: transparent;
242
+
243
+ &:hover:hover:hover {
244
+ background: ${token.colorFillSecondary};
245
+ }
246
+
247
+ &:active:active:active {
248
+ background: ${token.colorFill};
249
+ }
250
+
251
+ &,
252
+ &:hover:hover:hover {
253
+ color: ${token.colorWhite};
254
+ }
255
+
256
+ > .${prefixCls}-btn-icon {
257
+ font-size: 18px;
258
+ }
259
+ `,
260
+ toolbarButtonSelected: css`
261
+ &,
262
+ &:hover:hover:hover {
263
+ background: ${token.colorBgContainer};
264
+ color: ${token.colorText};
265
+ }
266
+
267
+ &::before {
268
+ content: "";
269
+ position: absolute;
270
+ inset: -1px;
271
+ border: inherit;
272
+ border-radius: inherit;
273
+ transition: inherit;
274
+ }
275
+
276
+ &:hover:hover:hover::before {
277
+ background: ${token.colorFillSecondary};
278
+ }
279
+
280
+ &:active:active:active::before {
281
+ background: ${token.colorFill};
282
+ }
283
+ `,
284
+ toolbarButtonText: css`
285
+ flex-basis: 100%;
286
+ text-align: left;
287
+ `,
288
+ };
289
+ });
290
+
291
+ interface ScalarInfo {
292
+ key: keyof WeatherData["data_vars"];
293
+ name: string;
294
+ unit: string;
295
+ icon: ReactNode;
296
+ adaptivePalette: AdaptivePalette;
297
+ fixedPalette?: [number, Vec3][];
298
+ }
299
+
300
+ const iconSizeFix = (ratio: number): CSSProperties => ({
301
+ fontSize: ratio + "em",
302
+ margin: -(1 - 1 / ratio) / 3 + "em " + -(1 - 1 / ratio) / 2 + "em",
303
+ });
304
+
305
+ const TEMPERATURE_ADAPTIVE_PALETTE: AdaptivePalette = {
306
+ min: [128, 65, 157],
307
+ zero: [239, 255, 45],
308
+ max: [254, 19, 12],
309
+ };
310
+
311
+ const TEMPERATURE_FIXED_PALETTE: [number, Vec3][] = [
312
+ [-18, [78, 138, 221]],
313
+ [-17, [87, 144, 222]],
314
+ [-16, [97, 150, 224]],
315
+ [-15, [106, 156, 225]],
316
+ [-14, [116, 163, 226]],
317
+ [-13, [125, 169, 227]],
318
+ [-12, [135, 175, 229]],
319
+ [-11, [145, 181, 230]],
320
+ [-10, [155, 188, 232]],
321
+ [-9, [154, 192, 226]],
322
+ [-8, [154, 196, 220]],
323
+ [-7, [154, 200, 214]],
324
+ [-6, [154, 205, 208]],
325
+ [-5, [153, 209, 202]],
326
+ [-4, [152, 214, 196]],
327
+ [-3, [151, 223, 184]],
328
+ [-2, [151, 232, 173]],
329
+ [-1, [183, 227, 149]],
330
+ [0, [215, 222, 125]],
331
+ [1, [224, 220, 118]],
332
+ [2, [234, 219, 112]],
333
+ [3, [239, 218, 105]],
334
+ [4, [244, 217, 99]],
335
+ [5, [247, 210, 89]],
336
+ [6, [250, 204, 79]],
337
+ [7, [248, 192, 62]],
338
+ [8, [247, 180, 45]],
339
+ [9, [244, 167, 22]],
340
+ [10, [242, 155, 0]],
341
+ [11, [241, 151, 1]],
342
+ [12, [241, 147, 3]],
343
+ [13, [240, 140, 6]],
344
+ [14, [240, 133, 10]],
345
+ [15, [239, 125, 13]],
346
+ [16, [239, 117, 17]],
347
+ [17, [238, 109, 20]],
348
+ [18, [238, 101, 24]],
349
+ [19, [238, 94, 27]],
350
+ [20, [238, 88, 31]],
351
+ [21, [234, 81, 28]],
352
+ [22, [231, 75, 26]],
353
+ [23, [227, 69, 24]],
354
+ [24, [224, 63, 22]],
355
+ [25, [220, 57, 20]],
356
+ [26, [217, 51, 18]],
357
+ [27, [212, 43, 16]],
358
+ [28, [208, 36, 14]],
359
+ [29, [201, 18, 8]],
360
+ [30, [194, 0, 3]],
361
+ [31, [187, 0, 6]],
362
+ [32, [181, 1, 9]],
363
+ [33, [175, 1, 12]],
364
+ [34, [169, 2, 16]],
365
+ [35, [153, 3, 20]],
366
+ [36, [138, 5, 25]],
367
+ [37, [124, 2, 23]],
368
+ [38, [111, 0, 21]],
369
+ [39, [95, 0, 18]],
370
+ [40, [80, 0, 15]],
371
+ [41, [70, 0, 15]],
372
+ [42, [60, 0, 15]],
373
+ [43, [50, 0, 15]],
374
+ [44, [40, 0, 15]],
375
+ [45, [30, 0, 15]],
376
+ [46, [20, 0, 15]],
377
+ ];
378
+
379
+ const SCALAR_VARIABLES: ScalarInfo[] = [
380
+ {
381
+ key: "temperature_2m",
382
+ name: "温度",
383
+ unit: "℃",
384
+ icon: <MdThermostat />,
385
+ adaptivePalette: TEMPERATURE_ADAPTIVE_PALETTE,
386
+ fixedPalette: TEMPERATURE_FIXED_PALETTE,
387
+ },
388
+ {
389
+ key: "surface_pressure",
390
+ name: "气压",
391
+ unit: "hPa",
392
+ icon: <WiBarometer style={iconSizeFix(1.4)} />,
393
+ adaptivePalette: {
394
+ min: [80, 202, 75],
395
+ max: [254, 19, 12],
396
+ },
397
+ },
398
+ {
399
+ key: "cloud_cover",
400
+ name: "云量",
401
+ unit: "%",
402
+ icon: <WiCloudy style={iconSizeFix(1.2)} />,
403
+ adaptivePalette: {
404
+ min: [239, 255, 45],
405
+ max: [255, 255, 255],
406
+ },
407
+ },
408
+ {
409
+ key: "dew_point_2m",
410
+ name: "露点",
411
+ unit: "℃",
412
+ icon: <MdDewPoint />,
413
+ adaptivePalette: TEMPERATURE_ADAPTIVE_PALETTE,
414
+ fixedPalette: TEMPERATURE_FIXED_PALETTE,
415
+ },
416
+ {
417
+ key: "relative_humidity_2m",
418
+ name: "相对湿度",
419
+ unit: "%",
420
+ icon: <WiHumidity style={iconSizeFix(1.3)} />,
421
+ adaptivePalette: {
422
+ min: [128, 65, 157],
423
+ zero: [239, 255, 45],
424
+ zeroPoint: 0.5,
425
+ max: [254, 19, 12],
426
+ },
427
+ },
428
+ {
429
+ key: "wind_speed_10m",
430
+ name: "风速",
431
+ unit: "m/s",
432
+ icon: <WiStrongWind />,
433
+ adaptivePalette: {
434
+ min: [239, 255, 45],
435
+ max: [128, 65, 157],
436
+ },
437
+ },
438
+ {
439
+ key: "rain",
440
+ name: "降雨量",
441
+ unit: "mm",
442
+ icon: <WiRain style={iconSizeFix(1.1)} />,
443
+ adaptivePalette: {
444
+ min: [80, 202, 75],
445
+ max: [254, 19, 12],
446
+ },
447
+ },
448
+ ];
449
+
450
+ function lerpVec<T extends number[]>(min: T, max: T, pos: number) {
451
+ return min.map((min, i) => min + pos * (max[i] - min)) as T;
452
+ }
453
+
454
+ type Vec3 = [number, number, number];
455
+
456
+ interface AdaptivePalette {
457
+ min: Vec3;
458
+ zero?: Vec3;
459
+ zeroPoint?: number;
460
+ max: Vec3;
461
+ }
462
+
463
+ // 参考 https://www.cma.gov.cn/zfxxgk/gknr/flfgbz/bz/202209/P020220921555079517928.pdf
464
+ function adaptiveColorCoding(
465
+ min: number,
466
+ max: number,
467
+ base: AdaptivePalette,
468
+ ): [number, Vec3][] {
469
+ if (base.zero) {
470
+ const palette: [number, Vec3][] = [];
471
+ const zeroPoint = base.zeroPoint ?? 0;
472
+ if (min < zeroPoint) {
473
+ palette.push([
474
+ min,
475
+ lerpVec(
476
+ base.zero,
477
+ base.min,
478
+ Math.min(1, Math.abs((min - zeroPoint) / (max - zeroPoint))),
479
+ ),
480
+ ]);
481
+ } else {
482
+ palette.push([min, base.zero]);
483
+ }
484
+ if (min < zeroPoint && max > zeroPoint) {
485
+ palette.push([zeroPoint, base.zero]);
486
+ }
487
+ if (max > zeroPoint) {
488
+ palette.push([
489
+ max,
490
+ lerpVec(
491
+ base.zero,
492
+ base.max,
493
+ Math.min(1, Math.abs((max - zeroPoint) / (min - zeroPoint))),
494
+ ),
495
+ ]);
496
+ } else {
497
+ palette.push([max, base.zero]);
498
+ }
499
+ return palette;
500
+ } else {
501
+ return [
502
+ [min, base.min],
503
+ [max, base.max],
504
+ ];
505
+ }
506
+ }
507
+
508
+ // 参考 https://dbba.sacinfo.org.cn/attachment/downloadStdFile?pk=dda6960be9435ed7ab01e336e197cd67a072a28725f03685bfbdaf19cbfeb267
509
+ function fixedColorCoding<T>(min: number, max: number, palette: [number, T][]) {
510
+ let minIdx = palette.findIndex(([value]) => value > min) - 1;
511
+ if (minIdx < -1) {
512
+ minIdx = palette.length - 1;
513
+ } else if (minIdx === -1) {
514
+ minIdx = 0;
515
+ }
516
+ let maxIdx = palette.findIndex(([value]) => value >= max);
517
+ if (maxIdx < 0) {
518
+ maxIdx = 0;
519
+ }
520
+ return palette.slice(minIdx, maxIdx + 1);
521
+ }
@@ -0,0 +1,56 @@
1
+ import { BitmapLayer, CompositeLayer } from "deck.gl";
2
+ import { TileLayer, type TileLayerProps } from "@deck.gl/geo-layers";
3
+ import type { DefaultProps } from "@deck.gl/core";
4
+
5
+ export interface TiandituLayerProps {
6
+ tiandituTk: string;
7
+ type: "vec" | "img" | "ter";
8
+ layer: "base" | "annotation";
9
+ }
10
+
11
+ export default class TiandituLayer extends CompositeLayer<TiandituLayerProps> {
12
+ static readonly layerName = "TiandituLayer";
13
+ static readonly defaultProps: DefaultProps<TiandituLayerProps> = {
14
+ type: { type: "object", value: "vec" },
15
+ layer: { type: "object", value: "base" },
16
+ };
17
+ renderLayers() {
18
+ const { tiandituTk, type, layer } = this.props;
19
+ const actualType = layer === "base" ? type : "c" + type[0] + "a";
20
+ return new TileLayer(
21
+ this.getSubLayerProps({
22
+ id: actualType,
23
+ data: makeUrlTemplate(tiandituTk, actualType),
24
+ maxZoom: 18,
25
+ zoomOffset: 1,
26
+ renderSubLayers,
27
+ }),
28
+ );
29
+ }
30
+ }
31
+
32
+ const makeUrlTemplate = (tiandituTk: string, type: string) =>
33
+ Array.from(
34
+ { length: 8 },
35
+ (_, i) =>
36
+ `https://t${i}.tianditu.gov.cn/${type}_w/wmts` +
37
+ `?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0` +
38
+ `&LAYER=${type}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles` +
39
+ `&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}` +
40
+ `&tk=${tiandituTk}`,
41
+ );
42
+
43
+ const renderSubLayers: TileLayerProps["renderSubLayers"] = (props) => {
44
+ const { boundingBox } = props.tile;
45
+ return new BitmapLayer({
46
+ ...props,
47
+ data: undefined,
48
+ image: props.data,
49
+ bounds: [
50
+ boundingBox[0][0],
51
+ boundingBox[0][1],
52
+ boundingBox[1][0],
53
+ boundingBox[1][1],
54
+ ],
55
+ });
56
+ };