@datagouv/components-next 1.0.2-dev.155 → 1.0.2-dev.157
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/dist/{Datafair.client-DtRZzOSC.js → Datafair.client-Bblv8Xec.js} +1 -1
- package/dist/ImagePreview.client-B9p_XZrx.js +36 -0
- package/dist/{JsonPreview.client-BUKUgRg5.js → JsonPreview.client-DzVXqgqm.js} +2 -2
- package/dist/{MapContainer.client-CJEunt9n.js → MapContainer.client-JFOchs0n.js} +2 -2
- package/dist/{PdfPreview.client-NEarDRLg.js → PdfPreview.client-De6XYp-a.js} +2 -2
- package/dist/{Pmtiles.client-Cd7YYm67.js → Pmtiles.client-CiLdMaAj.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-CxKTCDXD.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-keE4Kr1e.js} +1 -1
- package/dist/{XmlPreview.client-B3d67EsR.js → XmlPreview.client-CdxaPXOa.js} +3 -3
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +50 -49
- package/dist/components.css +1 -1
- package/dist/{index-Brxv657p.js → index-BIk8VKNQ.js} +1 -1
- package/dist/{leaflet-src-C4j3Fhuc.js → leaflet-src-B_fapURG.js} +1 -1
- package/dist/{main-B3yaCPy_.js → main-B0TVT1sb.js} +8296 -8269
- package/dist/{vue3-xml-viewer.common-BFa35npg.js → vue3-xml-viewer.common-Bx0jB4Yr.js} +1 -1
- package/package.json +1 -1
- package/src/components/Chart/ChartViewer.vue +97 -10
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +8 -3
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +8 -1
- package/src/components/TabularExplorer/types.ts +2 -2
- package/src/composables/useResourceCapabilities.ts +7 -7
- package/src/config.ts +1 -0
- package/src/functions/charts.ts +18 -2
- package/src/functions/resources.ts +7 -0
- package/src/types/visualizations.ts +1 -1
package/package.json
CHANGED
|
@@ -13,7 +13,8 @@ import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, Datas
|
|
|
13
13
|
import { init, type ECharts as EChartsType } from 'echarts'
|
|
14
14
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
15
15
|
import { summarize } from '../../functions/helpers'
|
|
16
|
-
import
|
|
16
|
+
import { useFormatDate } from '../../functions/dates'
|
|
17
|
+
import type { Chart, ColumnDefinition, ColumnsDefinition, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
|
|
17
18
|
import { useTranslation } from '../../composables/useTranslation'
|
|
18
19
|
|
|
19
20
|
use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
|
|
@@ -22,11 +23,12 @@ const props = defineProps<{
|
|
|
22
23
|
chart: Chart | ChartForApi
|
|
23
24
|
series: {
|
|
24
25
|
data: Record<string, Array<Record<string, unknown>>>
|
|
25
|
-
columns:
|
|
26
|
+
columns: ColumnsDefinition
|
|
26
27
|
}
|
|
27
28
|
}>()
|
|
28
29
|
|
|
29
30
|
const { locale } = useTranslation()
|
|
31
|
+
const { formatDate } = useFormatDate()
|
|
30
32
|
const chartContainer = ref<HTMLElement | null>(null)
|
|
31
33
|
let echartsInstance: EChartsType | null = null
|
|
32
34
|
|
|
@@ -36,9 +38,11 @@ function capture(): string | null {
|
|
|
36
38
|
|
|
37
39
|
defineExpose({ capture })
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
// ECharts has a dedicated 'time' axis type for continuous date values.
|
|
42
|
+
function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' | 'time' {
|
|
40
43
|
if (!xAxis) return 'category'
|
|
41
|
-
|
|
44
|
+
if (xAxis.type !== 'continuous') return 'category'
|
|
45
|
+
return isDateAxis.value ? 'time' : 'value'
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
|
|
@@ -50,6 +54,77 @@ function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefi
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
function buildXAxisFormatter(column: ColumnDefinition | null, options?: Intl.DateTimeFormatOptions): ((value: string | number | Date) => string) | undefined {
|
|
58
|
+
if (column?.type !== 'date') return undefined
|
|
59
|
+
return (value: string | number | Date) => {
|
|
60
|
+
if (value === null || value === undefined) return ''
|
|
61
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
62
|
+
if (Number.isNaN(date.getTime())) return String(value)
|
|
63
|
+
return formatDate(date, options ?? { dateStyle: 'short' })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseXValueAsDate(row: Record<string, unknown>, xColumn: string): Record<string, unknown> {
|
|
68
|
+
const value = row[xColumn]
|
|
69
|
+
if (value === null || value === undefined || value instanceof Date) return row
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
const date = new Date(value)
|
|
72
|
+
if (!Number.isNaN(date.getTime())) {
|
|
73
|
+
return { ...row, [xColumn]: date }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return row
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const xAxisColumn = computed(() => {
|
|
80
|
+
const columnName = props.chart.x_axis.column_x
|
|
81
|
+
if (!columnName) return null
|
|
82
|
+
for (const columns of Object.values(props.series.columns)) {
|
|
83
|
+
const column = columns.find(c => c.name === columnName)
|
|
84
|
+
if (column) return column
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const isDateAxis = computed(() => xAxisColumn.value?.type === 'date')
|
|
90
|
+
|
|
91
|
+
const xAxisBounds = computed(() => {
|
|
92
|
+
const columnName = props.chart.x_axis.column_x
|
|
93
|
+
if (!columnName) return { min: undefined, max: undefined }
|
|
94
|
+
|
|
95
|
+
let min: number | undefined
|
|
96
|
+
let max: number | undefined
|
|
97
|
+
|
|
98
|
+
for (const s of props.chart.series) {
|
|
99
|
+
const xColumn = s.column_x_name_override ?? columnName
|
|
100
|
+
const data = props.series.data[s.resource_id]
|
|
101
|
+
if (!data) continue
|
|
102
|
+
|
|
103
|
+
for (const row of data) {
|
|
104
|
+
const value = row[xColumn]
|
|
105
|
+
if (value === null || value === undefined) continue
|
|
106
|
+
|
|
107
|
+
let numericValue: number | undefined
|
|
108
|
+
if (value instanceof Date) {
|
|
109
|
+
numericValue = value.getTime()
|
|
110
|
+
}
|
|
111
|
+
else if (isDateAxis.value && typeof value === 'string') {
|
|
112
|
+
numericValue = new Date(value).getTime()
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
numericValue = Number(value)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (numericValue === undefined || Number.isNaN(numericValue)) continue
|
|
119
|
+
|
|
120
|
+
if (min === undefined || numericValue < min) min = numericValue
|
|
121
|
+
if (max === undefined || numericValue > max) max = numericValue
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { min, max }
|
|
126
|
+
})
|
|
127
|
+
|
|
53
128
|
const echartsOption = computed(() => {
|
|
54
129
|
const seriesCount = props.chart.series.length
|
|
55
130
|
if (!props.chart.series || seriesCount === 0) return
|
|
@@ -64,12 +139,15 @@ const echartsOption = computed(() => {
|
|
|
64
139
|
return null
|
|
65
140
|
}
|
|
66
141
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
142
|
+
const rawData = props.series.data[resourceId]
|
|
143
|
+
const sortedData = isDateAxis.value
|
|
144
|
+
? rawData.map(row => parseXValueAsDate(row, xColumn))
|
|
145
|
+
: [...rawData]
|
|
146
|
+
const sortBy = props.chart.x_axis.sort_x_by ?? (isDateAxis.value ? 'axis_x' : null)
|
|
69
147
|
const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
|
|
70
148
|
|
|
71
149
|
if (sortBy && sortDirection && props.chart.x_axis.column_x) {
|
|
72
|
-
const sortKey = sortBy === '
|
|
150
|
+
const sortKey = sortBy === 'axis_y' ? yColumn : xColumn
|
|
73
151
|
sortedData.sort((a, b) => {
|
|
74
152
|
const valA = a[sortKey]
|
|
75
153
|
const valB = b[sortKey]
|
|
@@ -142,6 +220,8 @@ const echartsOption = computed(() => {
|
|
|
142
220
|
seriesData.data.push(curr.data)
|
|
143
221
|
}
|
|
144
222
|
|
|
223
|
+
const xAxisType = mapXAxisType(props.chart.x_axis)
|
|
224
|
+
|
|
145
225
|
return {
|
|
146
226
|
dataset: [...seriesData.data],
|
|
147
227
|
textStyle: {
|
|
@@ -152,10 +232,12 @@ const echartsOption = computed(() => {
|
|
|
152
232
|
trigger: 'axis' as const,
|
|
153
233
|
formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
|
|
154
234
|
let tooltip = ''
|
|
155
|
-
const
|
|
235
|
+
const valueFormatter = new Intl.NumberFormat(locale)
|
|
236
|
+
const xAxisFormatter = buildXAxisFormatter(xAxisColumn.value, { dateStyle: 'medium' })
|
|
156
237
|
for (const param of params) {
|
|
157
238
|
const seriesName = param.seriesName
|
|
158
|
-
|
|
239
|
+
const axisLabel = xAxisFormatter ? xAxisFormatter(param.axisValueLabel) : param.axisValueLabel
|
|
240
|
+
tooltip += `${format.encodeHTML(String(axisLabel))}: <strong>${valueFormatter.format(Number(param.value[seriesName]))}</strong><br>`
|
|
159
241
|
}
|
|
160
242
|
return tooltip
|
|
161
243
|
},
|
|
@@ -172,8 +254,13 @@ const echartsOption = computed(() => {
|
|
|
172
254
|
containLabel: true,
|
|
173
255
|
},
|
|
174
256
|
xAxis: {
|
|
175
|
-
type:
|
|
257
|
+
type: xAxisType,
|
|
176
258
|
name: (props.chart.x_axis as XAxis).column_x,
|
|
259
|
+
min: xAxisType !== 'category' ? xAxisColumn.value?.min ?? xAxisBounds.value.min ?? undefined : undefined,
|
|
260
|
+
max: xAxisType !== 'category' ? xAxisColumn.value?.max ?? xAxisBounds.value.max ?? undefined : undefined,
|
|
261
|
+
axisLabel: xAxisType === 'time'
|
|
262
|
+
? { formatter: buildXAxisFormatter(xAxisColumn.value) }
|
|
263
|
+
: undefined,
|
|
177
264
|
},
|
|
178
265
|
yAxis: {
|
|
179
266
|
type: 'value' as const,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-xs">
|
|
3
|
+
<PreviewUnavailable v-if="!isSizeAllowed">
|
|
4
|
+
{{ fileSizeBytes
|
|
5
|
+
? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.", { fileType })
|
|
6
|
+
: t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.")
|
|
7
|
+
}}
|
|
8
|
+
</PreviewUnavailable>
|
|
9
|
+
<PreviewUnavailable v-else-if="hasError">
|
|
10
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
|
|
11
|
+
</PreviewUnavailable>
|
|
12
|
+
<img
|
|
13
|
+
v-else
|
|
14
|
+
:src="resource.url"
|
|
15
|
+
alt=""
|
|
16
|
+
class="block w-full h-auto max-w-[800px]"
|
|
17
|
+
@error="hasError = true"
|
|
18
|
+
>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { computed, ref } from 'vue'
|
|
24
|
+
import PreviewUnavailable from './PreviewUnavailable.vue'
|
|
25
|
+
import type { Resource } from '../../types/resources'
|
|
26
|
+
import { getResourceFilesize } from '../../functions/resources'
|
|
27
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
28
|
+
import { useComponentsConfig } from '../../config'
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
resource: Resource
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const { t } = useTranslation()
|
|
35
|
+
const config = useComponentsConfig()
|
|
36
|
+
|
|
37
|
+
const hasError = ref(false)
|
|
38
|
+
|
|
39
|
+
const fileType = computed(() => props.resource.format?.toUpperCase() ?? t('Fichier'))
|
|
40
|
+
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
|
|
41
|
+
|
|
42
|
+
const isSizeAllowed = computed(() => {
|
|
43
|
+
const size = fileSizeBytes.value
|
|
44
|
+
const max = config.maxImagePreviewByteSize
|
|
45
|
+
if (!size || !max) return false
|
|
46
|
+
return size <= max
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
@@ -224,6 +224,10 @@
|
|
|
224
224
|
v-else-if="resource.format && resource.format.toLowerCase() === 'xml'"
|
|
225
225
|
:resource="resource"
|
|
226
226
|
/>
|
|
227
|
+
<ImagePreview
|
|
228
|
+
v-else-if="isImagePreviewFormat(resource.format)"
|
|
229
|
+
:resource="resource"
|
|
230
|
+
/>
|
|
227
231
|
<!-- Show Datafair embedded preview (koumoul) -->
|
|
228
232
|
<DatafairPreview
|
|
229
233
|
v-else-if="hasDatafairPreview"
|
|
@@ -309,7 +313,7 @@ import { trackEvent } from '../../functions/matomo'
|
|
|
309
313
|
import CopyButton from '../CopyButton.vue'
|
|
310
314
|
import { useComponentsConfig } from '../../config'
|
|
311
315
|
import { getOwnerName } from '../../functions/owned'
|
|
312
|
-
import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
|
|
316
|
+
import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize, isImagePreviewFormat } from '../../functions/resources'
|
|
313
317
|
import BrandedButton from '../BrandedButton.vue'
|
|
314
318
|
import { useTranslation } from '../../composables/useTranslation'
|
|
315
319
|
import { useHasTabularData } from '../../composables/useHasTabularData'
|
|
@@ -344,6 +348,7 @@ const Pmtiles = defineAsyncComponent(() => import('./Pmtiles.client.vue'))
|
|
|
344
348
|
const JsonPreview = defineAsyncComponent(() => import('./JsonPreview.client.vue'))
|
|
345
349
|
const PdfPreview = defineAsyncComponent(() => import('./PdfPreview.client.vue'))
|
|
346
350
|
const XmlPreview = defineAsyncComponent(() => import('./XmlPreview.client.vue'))
|
|
351
|
+
const ImagePreview = defineAsyncComponent(() => import('./ImagePreview.client.vue'))
|
|
347
352
|
const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue'))
|
|
348
353
|
|
|
349
354
|
const { t } = useTranslation()
|
|
@@ -351,11 +356,11 @@ const { formatRelativeIfRecentDate } = useFormatDate()
|
|
|
351
356
|
const checkTabularData = useHasTabularData()
|
|
352
357
|
|
|
353
358
|
const hasPreview = computed(() => {
|
|
354
|
-
// For JSON, PDF, and
|
|
359
|
+
// For JSON, PDF, XML, and image files, show preview.
|
|
355
360
|
// We cannot check for CORS issues here because we cannot use an async component here.
|
|
356
361
|
// If there is a CORS issue when fetching the file for preview, it will be managed and displayed as an error banner by the preview component.
|
|
357
362
|
const format = props.resource.format?.toLowerCase()
|
|
358
|
-
return format === 'json' || format === 'pdf' || format === 'xml'
|
|
363
|
+
return format === 'json' || format === 'pdf' || format === 'xml' || isImagePreviewFormat(format)
|
|
359
364
|
})
|
|
360
365
|
|
|
361
366
|
const hasTabularData = computed(() => checkTabularData(props.resource))
|
|
@@ -141,6 +141,10 @@
|
|
|
141
141
|
v-else-if="resource.format && resource.format.toLowerCase() === 'xml'"
|
|
142
142
|
:resource="resource"
|
|
143
143
|
/>
|
|
144
|
+
<ImagePreview
|
|
145
|
+
v-else-if="isImagePreviewFormat(resource.format)"
|
|
146
|
+
:resource="resource"
|
|
147
|
+
/>
|
|
144
148
|
<DatafairPreview
|
|
145
149
|
v-else-if="hasDatafairPreview"
|
|
146
150
|
:resource="resource"
|
|
@@ -234,7 +238,7 @@ import Metadata from '../ResourceAccordion/Metadata.vue'
|
|
|
234
238
|
import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
|
|
235
239
|
import ResourceSelector from './ResourceSelector.vue'
|
|
236
240
|
import { filesize, summarize } from '../../functions/helpers'
|
|
237
|
-
import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
|
|
241
|
+
import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize, isImagePreviewFormat } from '../../functions/resources'
|
|
238
242
|
import { trackEvent } from '../../functions/matomo'
|
|
239
243
|
import { useComponentsConfig } from '../../config'
|
|
240
244
|
import { useFormatDate } from '../../functions/dates'
|
|
@@ -253,6 +257,9 @@ const PdfPreview = defineAsyncComponent(() =>
|
|
|
253
257
|
const XmlPreview = defineAsyncComponent(() =>
|
|
254
258
|
import('../ResourceAccordion/XmlPreview.client.vue'),
|
|
255
259
|
)
|
|
260
|
+
const ImagePreview = defineAsyncComponent(() =>
|
|
261
|
+
import('../ResourceAccordion/ImagePreview.client.vue'),
|
|
262
|
+
)
|
|
256
263
|
const DatafairPreview = defineAsyncComponent(() =>
|
|
257
264
|
import('../ResourceAccordion/Datafair.client.vue'),
|
|
258
265
|
)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
|
2
2
|
import { useComponentsConfig } from '../config'
|
|
3
|
-
import { useTranslation } from './useTranslation'
|
|
4
|
-
import { useHasTabularData } from './useHasTabularData'
|
|
5
|
-
import { detectOgcService, getParsingErrorMessage, getParsingErrorStep } from '../functions/resources'
|
|
6
3
|
import { isOrganizationCertified } from '../functions/organizations'
|
|
7
|
-
import type { Resource, WfsMetadata } from '../types/resources'
|
|
8
|
-
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
9
4
|
import { getWfsExportFormats } from '../functions/resourceCapabilities'
|
|
5
|
+
import { detectOgcService, getParsingErrorMessage, getParsingErrorStep, isImagePreviewFormat } from '../functions/resources'
|
|
6
|
+
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
7
|
+
import type { Resource, WfsMetadata } from '../types/resources'
|
|
8
|
+
import { useHasTabularData } from './useHasTabularData'
|
|
9
|
+
import { useTranslation } from './useTranslation'
|
|
10
10
|
|
|
11
11
|
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
12
12
|
const URL_FORMATS = ['url', 'doi', 'www:link', 'www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
|
|
@@ -21,7 +21,7 @@ export function useResourceCapabilities(
|
|
|
21
21
|
|
|
22
22
|
const hasPreview = computed(() => {
|
|
23
23
|
const format = toValue(resource).format?.toLowerCase()
|
|
24
|
-
return format === 'json' || format === 'pdf' || format === 'xml'
|
|
24
|
+
return format === 'json' || format === 'pdf' || format === 'xml' || isImagePreviewFormat(format)
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
const hasTabularData = computed(() => {
|
package/src/config.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type PluginConfig = {
|
|
|
12
12
|
datasetQualityGuideUrl?: string
|
|
13
13
|
maxJsonPreviewCharSize?: number // Maximum size of JSON to preview in characters. JSON preview module is partly collapsed by default so we can have a preview for large files.
|
|
14
14
|
maxPdfPreviewByteSize?: number // Maximum size of PDF to preview in bytes
|
|
15
|
+
maxImagePreviewByteSize?: number // Maximum size of image to preview in bytes
|
|
15
16
|
maxXmlPreviewCharSize?: number // Maximum size of XML to preview in characters. XML preview module can NOT be collapsed by default so we should not have a preview for large files.
|
|
16
17
|
pmtilesViewerBaseUrl?: string | null // Base URL of a pmtiles viewer (ex: https://pmtiles.io/#url=)
|
|
17
18
|
metricsApiUrl?: string
|
package/src/functions/charts.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Chart, ChartForm, ChartForApi, Filter, ColumnDefinition } from '../types/visualizations'
|
|
2
|
-
import type { TabularProfile } from '../components/TabularExplorer/types'
|
|
2
|
+
import type { ColumnType, TabularProfile } from '../components/TabularExplorer/types'
|
|
3
3
|
import { resolveColumnType } from './tabular'
|
|
4
4
|
|
|
5
5
|
export function toChartForm(chart: Chart) {
|
|
@@ -74,6 +74,22 @@ export function buildColumnsFromProfile(profile: { profile: TabularProfile }): A
|
|
|
74
74
|
const colInfo = profile.profile.columns[name]
|
|
75
75
|
const isCategorical = profile.profile.categorical.includes(name)
|
|
76
76
|
const colType = resolveColumnType(colInfo ?? { python_type: 'unknown', format: undefined }, isCategorical)
|
|
77
|
-
|
|
77
|
+
const colProfile = profile.profile.profile[name]
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
type: colType,
|
|
81
|
+
min: normalizeProfileNumber(colProfile?.min, colType),
|
|
82
|
+
max: normalizeProfileNumber(colProfile?.max, colType),
|
|
83
|
+
}
|
|
78
84
|
})
|
|
79
85
|
}
|
|
86
|
+
|
|
87
|
+
function normalizeProfileNumber(value: number | string | undefined, type: ColumnType): number | undefined {
|
|
88
|
+
if (value === undefined || value === null) return undefined
|
|
89
|
+
if (type === 'date' && typeof value === 'string') {
|
|
90
|
+
const timestamp = new Date(value).getTime()
|
|
91
|
+
return Number.isNaN(timestamp) ? undefined : timestamp
|
|
92
|
+
}
|
|
93
|
+
const parsed = Number(value)
|
|
94
|
+
return Number.isNaN(parsed) ? undefined : parsed
|
|
95
|
+
}
|
|
@@ -167,6 +167,13 @@ export function getResourceFilesize(resource: Resource): null | number {
|
|
|
167
167
|
return null
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
const IMAGE_PREVIEW_FORMATS = ['png', 'jpg', 'jpeg', 'jpe', 'gif', 'webp'] as const
|
|
171
|
+
|
|
172
|
+
export function isImagePreviewFormat(format: string | undefined | null): boolean {
|
|
173
|
+
if (!format) return false
|
|
174
|
+
return IMAGE_PREVIEW_FORMATS.includes(format.toLowerCase() as typeof IMAGE_PREVIEW_FORMATS[number])
|
|
175
|
+
}
|
|
176
|
+
|
|
170
177
|
// Parsing errors are stored as `<step>:<message>`, where `<step>` names the
|
|
171
178
|
// analysis step that failed (e.g. `pmtiles_export`, `geojson_export`).
|
|
172
179
|
|
|
@@ -89,5 +89,5 @@ export type ChartForm = Omit<ChartForApi, 'x_axis' | 'series' | 'owner' | 'organ
|
|
|
89
89
|
filter: GenericFilter | null
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export type ColumnDefinition = { name: string, type: ColumnType }
|
|
92
|
+
export type ColumnDefinition = { name: string, type: ColumnType, min?: number, max?: number }
|
|
93
93
|
export type ColumnsDefinition = Record<string, Array<ColumnDefinition>>
|