@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.
Files changed (26) hide show
  1. package/dist/{Datafair.client-DtRZzOSC.js → Datafair.client-Bblv8Xec.js} +1 -1
  2. package/dist/ImagePreview.client-B9p_XZrx.js +36 -0
  3. package/dist/{JsonPreview.client-BUKUgRg5.js → JsonPreview.client-DzVXqgqm.js} +2 -2
  4. package/dist/{MapContainer.client-CJEunt9n.js → MapContainer.client-JFOchs0n.js} +2 -2
  5. package/dist/{PdfPreview.client-NEarDRLg.js → PdfPreview.client-De6XYp-a.js} +2 -2
  6. package/dist/{Pmtiles.client-Cd7YYm67.js → Pmtiles.client-CiLdMaAj.js} +1 -1
  7. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-CxKTCDXD.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-keE4Kr1e.js} +1 -1
  8. package/dist/{XmlPreview.client-B3d67EsR.js → XmlPreview.client-CdxaPXOa.js} +3 -3
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +50 -49
  11. package/dist/components.css +1 -1
  12. package/dist/{index-Brxv657p.js → index-BIk8VKNQ.js} +1 -1
  13. package/dist/{leaflet-src-C4j3Fhuc.js → leaflet-src-B_fapURG.js} +1 -1
  14. package/dist/{main-B3yaCPy_.js → main-B0TVT1sb.js} +8296 -8269
  15. package/dist/{vue3-xml-viewer.common-BFa35npg.js → vue3-xml-viewer.common-Bx0jB4Yr.js} +1 -1
  16. package/package.json +1 -1
  17. package/src/components/Chart/ChartViewer.vue +97 -10
  18. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  19. package/src/components/ResourceAccordion/ResourceAccordion.vue +8 -3
  20. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +8 -1
  21. package/src/components/TabularExplorer/types.ts +2 -2
  22. package/src/composables/useResourceCapabilities.ts +7 -7
  23. package/src/config.ts +1 -0
  24. package/src/functions/charts.ts +18 -2
  25. package/src/functions/resources.ts +7 -0
  26. package/src/types/visualizations.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-B3yaCPy_.js";
1
+ import { g as Ke } from "./main-B0TVT1sb.js";
2
2
  import We from "vue";
3
3
  function Fe($, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.155",
3
+ "version": "1.0.2-dev.157",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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 type { Chart, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
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: Record<string, Array<{ name: string, type: string }>>
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
- function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' {
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
- return xAxis.type === 'continuous' ? 'value' : 'category'
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 sortedData = [...props.series.data[resourceId]]
68
- const sortBy = props.chart.x_axis.sort_x_by
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 === 'axis_x' ? xColumn : yColumn
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 formatter = new Intl.NumberFormat('fr-FR')
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
- tooltip += `${format.encodeHTML(param.axisValueLabel)}: <strong>${formatter.format(Number(param.value[seriesName]))}</strong><br>`
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: mapXAxisType(props.chart.x_axis),
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 XML files, show preview.
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
  )
@@ -51,8 +51,8 @@ export type TabularColumnProfile = {
51
51
  tops: TabularTopValue[]
52
52
  nb_distinct: number
53
53
  nb_missing_values: number
54
- min?: number
55
- max?: number
54
+ min?: number | string
55
+ max?: number | string
56
56
  std?: number
57
57
  mean?: number
58
58
  }
@@ -1,12 +1,12 @@
1
- import { computed, toValue, type MaybeRefOrGetter } from 'vue'
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
@@ -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
- return { name, type: colType }
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>>