zero2geospatial3d-sdk 0.1.0__py3-none-any.whl
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.
- zero2geospatial3d/__init__.py +105 -0
- zero2geospatial3d/acoustics.py +284 -0
- zero2geospatial3d/classify.py +379 -0
- zero2geospatial3d/cli.py +367 -0
- zero2geospatial3d/comfort.py +687 -0
- zero2geospatial3d/data/epw_stations.json.gz +0 -0
- zero2geospatial3d/daylight.py +337 -0
- zero2geospatial3d/dispersion.py +249 -0
- zero2geospatial3d/epw.py +162 -0
- zero2geospatial3d/geo.py +138 -0
- zero2geospatial3d/grid.py +119 -0
- zero2geospatial3d/microclimate.py +388 -0
- zero2geospatial3d/morphology.py +283 -0
- zero2geospatial3d/nsga2.py +315 -0
- zero2geospatial3d/osm.py +450 -0
- zero2geospatial3d/osm_tags.py +180 -0
- zero2geospatial3d/py.typed +0 -0
- zero2geospatial3d/results.py +231 -0
- zero2geospatial3d/road_sources.py +141 -0
- zero2geospatial3d/scene.py +555 -0
- zero2geospatial3d/server.py +332 -0
- zero2geospatial3d/shading.py +443 -0
- zero2geospatial3d/solar.py +207 -0
- zero2geospatial3d/terrain.py +610 -0
- zero2geospatial3d/urban_form.py +326 -0
- zero2geospatial3d/walkability.py +380 -0
- zero2geospatial3d/weather.py +373 -0
- zero2geospatial3d/web/index.html +50 -0
- zero2geospatial3d/web/src/analysis/climate.js +278 -0
- zero2geospatial3d/web/src/analysis/compliance.js +345 -0
- zero2geospatial3d/web/src/analysis/gpu.js +431 -0
- zero2geospatial3d/web/src/analysis/optimize.js +208 -0
- zero2geospatial3d/web/src/analysis/runners.js +1644 -0
- zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
- zero2geospatial3d/web/src/app.js +630 -0
- zero2geospatial3d/web/src/core/acoustics.js +341 -0
- zero2geospatial3d/web/src/core/classify.js +158 -0
- zero2geospatial3d/web/src/core/comfort.js +686 -0
- zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
- zero2geospatial3d/web/src/core/dispersion.js +264 -0
- zero2geospatial3d/web/src/core/microclimate.js +372 -0
- zero2geospatial3d/web/src/core/morphology.js +153 -0
- zero2geospatial3d/web/src/core/nsga2.js +181 -0
- zero2geospatial3d/web/src/core/party_walls.js +60 -0
- zero2geospatial3d/web/src/core/road_sources.js +127 -0
- zero2geospatial3d/web/src/core/solar.js +112 -0
- zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
- zero2geospatial3d/web/src/core/terrain_math.js +279 -0
- zero2geospatial3d/web/src/core/urban_form.js +405 -0
- zero2geospatial3d/web/src/core/walkability.js +316 -0
- zero2geospatial3d/web/src/core/weather.js +225 -0
- zero2geospatial3d/web/src/core/wind.js +131 -0
- zero2geospatial3d/web/src/export/check_pdf.js +74 -0
- zero2geospatial3d/web/src/export/exporters.js +556 -0
- zero2geospatial3d/web/src/export/pdf.js +149 -0
- zero2geospatial3d/web/src/export/snapshot.js +234 -0
- zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
- zero2geospatial3d/web/src/main.js +75 -0
- zero2geospatial3d/web/src/scene/objects.js +645 -0
- zero2geospatial3d/web/src/scene/overlay.js +145 -0
- zero2geospatial3d/web/src/scene/terrain.js +546 -0
- zero2geospatial3d/web/src/scene/viewer.js +275 -0
- zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
- zero2geospatial3d/web/src/style.css +225 -0
- zero2geospatial3d/web/src/ui/building_report.js +404 -0
- zero2geospatial3d/web/src/ui/charts.js +441 -0
- zero2geospatial3d/web/src/ui/colorize.js +101 -0
- zero2geospatial3d/web/src/ui/compare.js +362 -0
- zero2geospatial3d/web/src/ui/dom.js +104 -0
- zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
- zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
- zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
- zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
- zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
- zero2geospatial3d/web/src/ui/sunclock.js +45 -0
- zero2geospatial3d/web/src/ui/tools.js +197 -0
- zero2geospatial3d/web/src/ui/viewtools.js +173 -0
- zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
- zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
- zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
- zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
- zero2geospatial3d/wind.py +178 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Colour ramps and classification for analysis results.
|
|
2
|
+
|
|
3
|
+
Equal-interval, quantile and Fisher-Jenks natural breaks, categories by
|
|
4
|
+
frequency, and the colour ramps used by the plugin's viewer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from typing import Any, Iterable, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"QUALITATIVE",
|
|
14
|
+
"RAMPS",
|
|
15
|
+
"RAMP_LABELS",
|
|
16
|
+
"breaks_for",
|
|
17
|
+
"categories",
|
|
18
|
+
"class_of",
|
|
19
|
+
"equal_breaks",
|
|
20
|
+
"fmt",
|
|
21
|
+
"hex_to_rgb",
|
|
22
|
+
"jenks_breaks",
|
|
23
|
+
"quantile_breaks",
|
|
24
|
+
"ramp_color",
|
|
25
|
+
"ramp_stops",
|
|
26
|
+
"rgb_to_hex",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
RAMPS: dict[str, list[str]] = {
|
|
30
|
+
"viridis": [
|
|
31
|
+
"#440154",
|
|
32
|
+
"#482878",
|
|
33
|
+
"#3e4989",
|
|
34
|
+
"#31688e",
|
|
35
|
+
"#26828e",
|
|
36
|
+
"#1f9e89",
|
|
37
|
+
"#35b779",
|
|
38
|
+
"#6ece58",
|
|
39
|
+
"#b5de2b",
|
|
40
|
+
"#fde725",
|
|
41
|
+
],
|
|
42
|
+
"magma": [
|
|
43
|
+
"#000004",
|
|
44
|
+
"#1c1044",
|
|
45
|
+
"#4f127b",
|
|
46
|
+
"#812581",
|
|
47
|
+
"#b5367a",
|
|
48
|
+
"#e55964",
|
|
49
|
+
"#fb8761",
|
|
50
|
+
"#fec287",
|
|
51
|
+
"#fcfdbf",
|
|
52
|
+
],
|
|
53
|
+
"inferno": [
|
|
54
|
+
"#000004",
|
|
55
|
+
"#1f0c48",
|
|
56
|
+
"#550f6d",
|
|
57
|
+
"#88226a",
|
|
58
|
+
"#ba3655",
|
|
59
|
+
"#e35933",
|
|
60
|
+
"#f98e09",
|
|
61
|
+
"#f9cb35",
|
|
62
|
+
"#fcffa4",
|
|
63
|
+
],
|
|
64
|
+
"rdylbu": [
|
|
65
|
+
"#a50026",
|
|
66
|
+
"#d73027",
|
|
67
|
+
"#f46d43",
|
|
68
|
+
"#fdae61",
|
|
69
|
+
"#fee090",
|
|
70
|
+
"#ffffbf",
|
|
71
|
+
"#e0f3f8",
|
|
72
|
+
"#abd9e9",
|
|
73
|
+
"#74add1",
|
|
74
|
+
"#4575b4",
|
|
75
|
+
"#313695",
|
|
76
|
+
],
|
|
77
|
+
"spectral": [
|
|
78
|
+
"#9e0142",
|
|
79
|
+
"#d53e4f",
|
|
80
|
+
"#f46d43",
|
|
81
|
+
"#fdae61",
|
|
82
|
+
"#fee08b",
|
|
83
|
+
"#ffffbf",
|
|
84
|
+
"#e6f598",
|
|
85
|
+
"#abdda4",
|
|
86
|
+
"#66c2a5",
|
|
87
|
+
"#3288bd",
|
|
88
|
+
"#5e4fa2",
|
|
89
|
+
],
|
|
90
|
+
"rdbu": [
|
|
91
|
+
"#67001f",
|
|
92
|
+
"#b2182b",
|
|
93
|
+
"#d6604d",
|
|
94
|
+
"#f4a582",
|
|
95
|
+
"#fddbc7",
|
|
96
|
+
"#f7f7f7",
|
|
97
|
+
"#d1e5f0",
|
|
98
|
+
"#92c5de",
|
|
99
|
+
"#4393c3",
|
|
100
|
+
"#2166ac",
|
|
101
|
+
"#053061",
|
|
102
|
+
],
|
|
103
|
+
"blues": [
|
|
104
|
+
"#f7fbff",
|
|
105
|
+
"#deebf7",
|
|
106
|
+
"#c6dbef",
|
|
107
|
+
"#9ecae1",
|
|
108
|
+
"#6baed6",
|
|
109
|
+
"#4292c6",
|
|
110
|
+
"#2171b5",
|
|
111
|
+
"#08519c",
|
|
112
|
+
"#08306b",
|
|
113
|
+
],
|
|
114
|
+
"reds": [
|
|
115
|
+
"#fff5f0",
|
|
116
|
+
"#fee0d2",
|
|
117
|
+
"#fcbba1",
|
|
118
|
+
"#fc9272",
|
|
119
|
+
"#fb6a4a",
|
|
120
|
+
"#ef3b2c",
|
|
121
|
+
"#cb181d",
|
|
122
|
+
"#a50f15",
|
|
123
|
+
"#67000d",
|
|
124
|
+
],
|
|
125
|
+
"greens": [
|
|
126
|
+
"#f7fcf5",
|
|
127
|
+
"#e5f5e0",
|
|
128
|
+
"#c7e9c0",
|
|
129
|
+
"#a1d99b",
|
|
130
|
+
"#74c476",
|
|
131
|
+
"#41ab5d",
|
|
132
|
+
"#238b45",
|
|
133
|
+
"#006d2c",
|
|
134
|
+
"#00441b",
|
|
135
|
+
],
|
|
136
|
+
"terrain": [
|
|
137
|
+
"#2b6b3f",
|
|
138
|
+
"#5f9a4f",
|
|
139
|
+
"#a8c47a",
|
|
140
|
+
"#e9dfa3",
|
|
141
|
+
"#d2b47c",
|
|
142
|
+
"#a47b55",
|
|
143
|
+
"#8a7a72",
|
|
144
|
+
"#f2f2f2",
|
|
145
|
+
],
|
|
146
|
+
"greys": [
|
|
147
|
+
"#000000",
|
|
148
|
+
"#262626",
|
|
149
|
+
"#4d4d4d",
|
|
150
|
+
"#737373",
|
|
151
|
+
"#969696",
|
|
152
|
+
"#bdbdbd",
|
|
153
|
+
"#d9d9d9",
|
|
154
|
+
"#f0f0f0",
|
|
155
|
+
"#ffffff",
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
QUALITATIVE: list[str] = [
|
|
159
|
+
"#4e79a7",
|
|
160
|
+
"#f28e2b",
|
|
161
|
+
"#e15759",
|
|
162
|
+
"#76b7b2",
|
|
163
|
+
"#59a14f",
|
|
164
|
+
"#edc948",
|
|
165
|
+
"#b07aa1",
|
|
166
|
+
"#ff9da7",
|
|
167
|
+
"#9c755f",
|
|
168
|
+
"#bab0ac",
|
|
169
|
+
"#1f77b4",
|
|
170
|
+
"#ff7f0e",
|
|
171
|
+
"#2ca02c",
|
|
172
|
+
"#d62728",
|
|
173
|
+
"#9467bd",
|
|
174
|
+
"#8c564b",
|
|
175
|
+
"#e377c2",
|
|
176
|
+
"#7f7f7f",
|
|
177
|
+
"#bcbd22",
|
|
178
|
+
"#17becf",
|
|
179
|
+
]
|
|
180
|
+
RAMP_LABELS: dict[str, str] = {
|
|
181
|
+
"viridis": "Viridis",
|
|
182
|
+
"magma": "Magma",
|
|
183
|
+
"inferno": "Inferno",
|
|
184
|
+
"rdylbu": "Red-Yellow-Blue",
|
|
185
|
+
"spectral": "Spectral",
|
|
186
|
+
"rdbu": "Red-Blue",
|
|
187
|
+
"blues": "Blues",
|
|
188
|
+
"reds": "Reds",
|
|
189
|
+
"greens": "Greens",
|
|
190
|
+
"terrain": "Terrain",
|
|
191
|
+
"greys": "Greys",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def hex_to_rgb(hex_color: Optional[str]) -> list[float]:
|
|
196
|
+
"""``'#rrggbb'`` (or ``'#rgb'``) to ``[r, g, b]`` in ``0..1``."""
|
|
197
|
+
h = str(hex_color or "#cccccc").replace("#", "")
|
|
198
|
+
if len(h) == 3:
|
|
199
|
+
h = "".join(ch + ch for ch in h)
|
|
200
|
+
try:
|
|
201
|
+
v = int(h[:6], 16)
|
|
202
|
+
except ValueError:
|
|
203
|
+
v = 0
|
|
204
|
+
return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _round_half_up(x: float) -> int:
|
|
208
|
+
return int(math.floor(x + 0.5))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def rgb_to_hex(rgb: Sequence[float]) -> str:
|
|
212
|
+
"""``[r, g, b]`` in ``0..1`` to ``'#rrggbb'``."""
|
|
213
|
+
return "#" + "".join(f"{_round_half_up(max(0.0, min(1.0, x)) * 255):02x}" for x in rgb[:3])
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def ramp_stops(name: str) -> list[list[float]]:
|
|
217
|
+
"""Ramp stops as RGB triples; a name ending in ``-r`` is reversed."""
|
|
218
|
+
name = str(name)
|
|
219
|
+
reversed_ = name.endswith("-r")
|
|
220
|
+
key = name[:-2] if reversed_ else name
|
|
221
|
+
stops = [hex_to_rgb(h) for h in RAMPS.get(key, RAMPS["viridis"])]
|
|
222
|
+
return stops[::-1] if reversed_ else stops
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def ramp_color(stops: Sequence[Sequence[float]], t: float) -> list[float]:
|
|
226
|
+
"""Colour at ``t`` in ``[0, 1]`` along a ramp (stops from :func:`ramp_stops`)."""
|
|
227
|
+
tt = t if isinstance(t, (int, float)) and math.isfinite(t) else 0.0
|
|
228
|
+
v = max(0.0, min(1.0, tt)) * (len(stops) - 1)
|
|
229
|
+
i = min(len(stops) - 2, int(math.floor(v)))
|
|
230
|
+
f = v - i
|
|
231
|
+
return [stops[i][k] + (stops[i + 1][k] - stops[i][k]) * f for k in range(3)]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _to_float(v: Any) -> Optional[float]:
|
|
235
|
+
if v is None or v == "":
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
x = float(v)
|
|
239
|
+
except (TypeError, ValueError):
|
|
240
|
+
return None
|
|
241
|
+
return x if math.isfinite(x) else None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _finite_sorted(values: Iterable[Any]) -> list[float]:
|
|
245
|
+
out = [x for x in (_to_float(v) for v in values) if x is not None]
|
|
246
|
+
return sorted(out)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def equal_breaks(values: Iterable[Any], k: int) -> list[float]:
|
|
250
|
+
"""``k`` equal-interval classes: ``k + 1`` break values."""
|
|
251
|
+
s = _finite_sorted(values)
|
|
252
|
+
if not s:
|
|
253
|
+
return []
|
|
254
|
+
lo, hi = s[0], s[-1]
|
|
255
|
+
return [lo + ((hi - lo) * i) / k for i in range(k + 1)]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def quantile_breaks(values: Iterable[Any], k: int) -> list[float]:
|
|
259
|
+
"""``k`` quantile classes: ``k + 1`` break values."""
|
|
260
|
+
s = _finite_sorted(values)
|
|
261
|
+
if not s:
|
|
262
|
+
return []
|
|
263
|
+
out = [s[0]]
|
|
264
|
+
for i in range(1, k):
|
|
265
|
+
out.append(s[min(len(s) - 1, (i * len(s)) // k)])
|
|
266
|
+
out.append(s[-1])
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def jenks_breaks(values: Iterable[Any], k: int) -> list[float]:
|
|
271
|
+
"""Fisher-Jenks natural breaks on at most 3000 sampled values."""
|
|
272
|
+
full = _finite_sorted(values)
|
|
273
|
+
s = full
|
|
274
|
+
if not s:
|
|
275
|
+
return []
|
|
276
|
+
if len(s) > 3000:
|
|
277
|
+
step = len(s) / 3000
|
|
278
|
+
s = [full[int(math.floor(i * step))] for i in range(3000)]
|
|
279
|
+
s.append(full[-1])
|
|
280
|
+
n = len(s)
|
|
281
|
+
k = max(1, min(k, len(set(s))))
|
|
282
|
+
inf = math.inf
|
|
283
|
+
lower = [[0] * (k + 1) for _ in range(n + 1)]
|
|
284
|
+
variance = [[inf] * (k + 1) for _ in range(n + 1)]
|
|
285
|
+
for j in range(1, k + 1):
|
|
286
|
+
lower[1][j] = 1
|
|
287
|
+
variance[1][j] = 0.0
|
|
288
|
+
for l_ in range(2, n + 1):
|
|
289
|
+
total = 0.0
|
|
290
|
+
total_sq = 0.0
|
|
291
|
+
w = 0
|
|
292
|
+
v = 0.0
|
|
293
|
+
var_l = variance[l_]
|
|
294
|
+
low_l = lower[l_]
|
|
295
|
+
for m in range(1, l_ + 1):
|
|
296
|
+
i3 = l_ - m + 1
|
|
297
|
+
val = s[i3 - 1]
|
|
298
|
+
w += 1
|
|
299
|
+
total += val
|
|
300
|
+
total_sq += val * val
|
|
301
|
+
v = total_sq - (total * total) / w
|
|
302
|
+
i4 = i3 - 1
|
|
303
|
+
if i4 != 0:
|
|
304
|
+
var_i4 = variance[i4]
|
|
305
|
+
for j in range(2, k + 1):
|
|
306
|
+
cand = v + var_i4[j - 1]
|
|
307
|
+
if var_l[j] >= cand:
|
|
308
|
+
low_l[j] = i3
|
|
309
|
+
var_l[j] = cand
|
|
310
|
+
low_l[1] = 1
|
|
311
|
+
var_l[1] = v
|
|
312
|
+
breaks = [0.0] * (k + 1)
|
|
313
|
+
breaks[k] = s[n - 1]
|
|
314
|
+
breaks[0] = s[0]
|
|
315
|
+
idx = n
|
|
316
|
+
for j in range(k, 1, -1):
|
|
317
|
+
ident = lower[idx][j] - 2
|
|
318
|
+
breaks[j - 1] = s[max(0, ident)]
|
|
319
|
+
idx = lower[idx][j] - 1
|
|
320
|
+
return breaks
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def breaks_for(method: str, values: Iterable[Any], k: int) -> list[float]:
|
|
324
|
+
"""Breaks by ``method``: ``'quantile'``, ``'jenks'`` or equal interval (default)."""
|
|
325
|
+
if method == "quantile":
|
|
326
|
+
return quantile_breaks(values, k)
|
|
327
|
+
if method == "jenks":
|
|
328
|
+
return jenks_breaks(values, k)
|
|
329
|
+
return equal_breaks(values, k)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def class_of(value: Any, breaks: Sequence[float]) -> int:
|
|
333
|
+
"""Class index ``0..k-1`` of a value for breaks ``[b0..bk]``, or ``-1``."""
|
|
334
|
+
v = _to_float(value)
|
|
335
|
+
if v is None or len(breaks) < 2:
|
|
336
|
+
return -1
|
|
337
|
+
for i in range(1, len(breaks) - 1):
|
|
338
|
+
if v < breaks[i]:
|
|
339
|
+
return i - 1
|
|
340
|
+
return len(breaks) - 2
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _key(v: Any) -> str:
|
|
344
|
+
if v is None or v == "":
|
|
345
|
+
return "(empty)"
|
|
346
|
+
if isinstance(v, bool):
|
|
347
|
+
return "true" if v else "false"
|
|
348
|
+
if isinstance(v, float) and v.is_integer():
|
|
349
|
+
return str(int(v))
|
|
350
|
+
return str(v)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def categories(values: Iterable[Any], limit: int = 20) -> list[dict[str, Any]]:
|
|
354
|
+
"""Distinct values by frequency, most common first (at most ``limit``)."""
|
|
355
|
+
counts: dict[str, int] = {}
|
|
356
|
+
for v in values:
|
|
357
|
+
key = _key(v)
|
|
358
|
+
counts[key] = counts.get(key, 0) + 1
|
|
359
|
+
ordered = sorted(counts.items(), key=lambda kv: -kv[1])[:limit]
|
|
360
|
+
return [{"value": value, "count": count} for value, count in ordered]
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def fmt(v: Any, unit: str = "") -> str:
|
|
364
|
+
"""Short number formatting for legends (``'–'`` for non-finite values)."""
|
|
365
|
+
if not isinstance(v, (int, float)) or isinstance(v, bool) or not math.isfinite(v):
|
|
366
|
+
return "–"
|
|
367
|
+
if float(v).is_integer():
|
|
368
|
+
s = f"{int(v):,}"
|
|
369
|
+
else:
|
|
370
|
+
a = abs(v)
|
|
371
|
+
if a >= 1000:
|
|
372
|
+
s = f"{_round_half_up(v):,}"
|
|
373
|
+
elif a >= 100:
|
|
374
|
+
s = f"{v:.0f}"
|
|
375
|
+
elif a >= 10:
|
|
376
|
+
s = f"{v:.1f}"
|
|
377
|
+
else:
|
|
378
|
+
s = f"{v:.2f}"
|
|
379
|
+
return f"{s} {unit}" if unit else s
|