@datagouv/components-next 1.0.2-dev.154 → 1.0.2-dev.156
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-CUK3pS6u.js → Datafair.client-bp15NLRY.js} +1 -1
- package/dist/{JsonPreview.client-DXCVNsnV.js → JsonPreview.client-yqVpZTmF.js} +2 -2
- package/dist/{MapContainer.client-BTwgsANA.js → MapContainer.client-2lywLnVm.js} +2 -2
- package/dist/{PdfPreview.client-B-W-HknR.js → PdfPreview.client-BG7MXWMB.js} +2 -2
- package/dist/{Pmtiles.client-CCNaGHFD.js → Pmtiles.client-5ghpJ7mq.js} +5647 -5500
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-Cu7pYpdZ.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-C-sK1BJV.js} +1 -1
- package/dist/{XmlPreview.client-CalqyfB_.js → XmlPreview.client-DWzKxXoL.js} +3 -3
- package/dist/components-next.js +98 -99
- package/dist/components.css +1 -1
- package/dist/{index-CeKxF_3e.js → index-B6iEXWF6.js} +148 -137
- package/dist/{leaflet-src-zZByibj1.js → leaflet-src-CpoKI-p2.js} +1 -1
- package/dist/{main-B4HZYPjy.js → main-uIKdSuDJ.js} +12426 -12478
- package/dist/{vue3-xml-viewer.common-omEYjVTS.js → vue3-xml-viewer.common-C2hQ2aXh.js} +1 -1
- package/package.json +2 -2
- package/src/components/Chart/ChartViewer.vue +97 -10
- package/src/components/TabularExplorer/TabularCell.vue +0 -9
- package/src/components/TabularExplorer/TabularCellPopover.vue +1 -1
- package/src/components/TabularExplorer/TabularExplorer.vue +4 -80
- package/src/components/TabularExplorer/TabularFilterContent.vue +3 -3
- package/src/components/TabularExplorer/types.ts +5 -5
- package/src/functions/charts.ts +18 -2
- package/src/functions/tabular.ts +1 -26
- package/src/types/visualizations.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datagouv/components-next",
|
|
3
|
-
"version": "1.0.2-dev.
|
|
3
|
+
"version": "1.0.2-dev.156",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@vueuse/core": "^14.3.0",
|
|
32
32
|
"@vueuse/router": "^14.3.0",
|
|
33
33
|
"chart.js": "^4.5.1",
|
|
34
|
-
"dompurify": "^3.4.
|
|
34
|
+
"dompurify": "^3.4.11",
|
|
35
35
|
"echarts": "^6.1.0",
|
|
36
36
|
"geopf-extensions-openlayers": "1.0.0-beta.11",
|
|
37
37
|
"leaflet": "^1.9.4",
|
|
@@ -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,
|
|
@@ -20,10 +20,6 @@
|
|
|
20
20
|
class="inline-block rounded font-medium px-2 py-0.5 text-xs max-w-full truncate"
|
|
21
21
|
:style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
|
|
22
22
|
>{{ value }}</span>
|
|
23
|
-
<span
|
|
24
|
-
v-else-if="columnType === 'number' && noNumberFormat"
|
|
25
|
-
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
26
|
-
>{{ value }}</span>
|
|
27
23
|
<span
|
|
28
24
|
v-else-if="columnType === 'number'"
|
|
29
25
|
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
@@ -32,10 +28,6 @@
|
|
|
32
28
|
v-else-if="columnType === 'date'"
|
|
33
29
|
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
34
30
|
>{{ formatCellDate(value) }}</span>
|
|
35
|
-
<span
|
|
36
|
-
v-else-if="columnType === 'year'"
|
|
37
|
-
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
38
|
-
>{{ value }}</span>
|
|
39
31
|
<span
|
|
40
32
|
v-else
|
|
41
33
|
class="text-gray-title truncate block text-xs"
|
|
@@ -52,7 +44,6 @@ defineProps<{
|
|
|
52
44
|
columnType: ColumnType
|
|
53
45
|
categoryBadgeStyle?: BadgeStyle
|
|
54
46
|
compact?: boolean
|
|
55
|
-
noNumberFormat?: boolean
|
|
56
47
|
}>()
|
|
57
48
|
|
|
58
49
|
const { t } = useTranslation()
|
|
@@ -128,7 +128,7 @@ function filterByValue() {
|
|
|
128
128
|
const val = String(cell.value.value ?? '')
|
|
129
129
|
const col = cell.value.column
|
|
130
130
|
const existing = filters.value[col] ?? {}
|
|
131
|
-
if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date'
|
|
131
|
+
if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date') {
|
|
132
132
|
const current = existing.in ?? []
|
|
133
133
|
if (!current.includes(val)) {
|
|
134
134
|
filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
ExploreResourceView), so they line up with the resource title /
|
|
64
64
|
download button above. Only the table itself goes edge-to-edge
|
|
65
65
|
(via -mx-4 below). -->
|
|
66
|
-
<slot name="toolbar-top" />
|
|
67
66
|
<div>
|
|
68
67
|
<div class="flex items-center py-3 gap-2">
|
|
69
68
|
<!-- Mobile: filter & sort button -->
|
|
@@ -380,32 +379,15 @@
|
|
|
380
379
|
v-for="col in displayedColumns"
|
|
381
380
|
:key="col"
|
|
382
381
|
data-cell
|
|
383
|
-
class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden"
|
|
384
|
-
:class="
|
|
385
|
-
!props.disablePopover && 'cursor-pointer',
|
|
386
|
-
{ 'text-right font-mono tabular-nums text-sm': getColumnType(col) === 'number' || getColumnType(col) === 'date' || getColumnType(col) === 'year' },
|
|
387
|
-
]"
|
|
382
|
+
class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden cursor-pointer hover:bg-gray-200/50"
|
|
383
|
+
:class="{ 'text-right font-mono tabular-nums text-sm': getColumnType(col) === 'number' || getColumnType(col) === 'date' }"
|
|
388
384
|
:style="columnWidths[col] ? { maxWidth: columnWidths[col] + 'px' } : { maxWidth: '300px' }"
|
|
389
385
|
@click="onCellClick(col, row[col], $event)"
|
|
390
386
|
>
|
|
391
|
-
<a
|
|
392
|
-
v-if="linkColumns?.has(col)"
|
|
393
|
-
:href="getRowHref(row) ?? ''"
|
|
394
|
-
class="link"
|
|
395
|
-
>
|
|
396
|
-
<TabularCell
|
|
397
|
-
:value="row[col]"
|
|
398
|
-
:column-type="getColumnType(col)"
|
|
399
|
-
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
400
|
-
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
401
|
-
/>
|
|
402
|
-
</a>
|
|
403
387
|
<TabularCell
|
|
404
|
-
v-else
|
|
405
388
|
:value="row[col]"
|
|
406
389
|
:column-type="getColumnType(col)"
|
|
407
390
|
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
408
|
-
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
409
391
|
/>
|
|
410
392
|
</td>
|
|
411
393
|
</tr>
|
|
@@ -420,7 +402,6 @@
|
|
|
420
402
|
|
|
421
403
|
<!-- Cell popover -->
|
|
422
404
|
<TabularCellPopover
|
|
423
|
-
v-if="!props.disablePopover"
|
|
424
405
|
v-model:cell="activeCell"
|
|
425
406
|
v-model:filters="filters"
|
|
426
407
|
/>
|
|
@@ -469,25 +450,11 @@
|
|
|
469
450
|
class="min-w-0 pl-4 cursor-pointer"
|
|
470
451
|
@click="onCellClick(col, row[col], $event)"
|
|
471
452
|
>
|
|
472
|
-
<a
|
|
473
|
-
v-if="linkColumns?.has(col)"
|
|
474
|
-
:href="getRowHref(row) ?? ''"
|
|
475
|
-
>
|
|
476
|
-
<TabularCell
|
|
477
|
-
:value="row[col]"
|
|
478
|
-
:column-type="getColumnType(col)"
|
|
479
|
-
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
480
|
-
compact
|
|
481
|
-
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
482
|
-
/>
|
|
483
|
-
</a>
|
|
484
453
|
<TabularCell
|
|
485
|
-
v-else
|
|
486
454
|
:value="row[col]"
|
|
487
455
|
:column-type="getColumnType(col)"
|
|
488
456
|
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
489
457
|
compact
|
|
490
|
-
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
491
458
|
/>
|
|
492
459
|
</div>
|
|
493
460
|
</div>
|
|
@@ -647,7 +614,7 @@ import { useFetch } from '../../functions/api'
|
|
|
647
614
|
import { useComponentsConfig } from '../../config'
|
|
648
615
|
import { useTranslation } from '../../composables/useTranslation'
|
|
649
616
|
import { injectTabularProfile } from '../../composables/useTabularProfile'
|
|
650
|
-
import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType
|
|
617
|
+
import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType } from '../../functions/tabular'
|
|
651
618
|
import ClientOnly from '../ClientOnly.vue'
|
|
652
619
|
import SimpleBanner from '../SimpleBanner.vue'
|
|
653
620
|
import BrandedButton from '../BrandedButton.vue'
|
|
@@ -666,37 +633,8 @@ const props = defineProps<{
|
|
|
666
633
|
// the host's padding. Used on the standalone explore page where the table
|
|
667
634
|
// should span the whole screen while the toolbar stays inside the container.
|
|
668
635
|
fullBleed?: boolean
|
|
669
|
-
// When set, searches across multiple columns using the Tabular API's or(...)
|
|
670
|
-
// parameter. Text and categorical columns get a __contains filter; number
|
|
671
|
-
// columns get a __exact filter (since __contains is not supported for numbers
|
|
672
|
-
// by the API). Date and boolean columns are excluded.
|
|
673
|
-
// Note: combined via AND with any existing column-specific `contains` filters,
|
|
674
|
-
// so it acts as an additional narrowing constraint, not a replacement.
|
|
675
|
-
globalSearch?: string
|
|
676
|
-
// When true, clicking a cell does not open the popover. Use when row-click
|
|
677
|
-
// navigation is the primary interaction.
|
|
678
|
-
disablePopover?: boolean
|
|
679
|
-
// Initial filters applied on mount, e.g. { 'Administration': { contains: 'Ministère' } }.
|
|
680
|
-
// Used when navigating from the detail page badge with query params.
|
|
681
|
-
initialFilters?: Record<string, ColumnFilters>
|
|
682
|
-
// When set, renders <a> tags inside the specified column cells for native
|
|
683
|
-
// browser UX (hover URL, ctrl+click, middle-click).
|
|
684
|
-
rowHref?: { columns: string[], href: (row: TabularRow) => string }
|
|
685
|
-
// Columns listed here render their values raw (no number formatting).
|
|
686
|
-
// Useful for identifier columns.
|
|
687
|
-
noFormatColumns?: string[]
|
|
688
636
|
}>()
|
|
689
637
|
|
|
690
|
-
const rowHrefFn = computed(() => props.rowHref?.href ?? null)
|
|
691
|
-
|
|
692
|
-
function getRowHref(row: TabularRow): string | null {
|
|
693
|
-
return rowHrefFn.value?.(row) ?? null
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const linkColumns = computed(() =>
|
|
697
|
-
props.rowHref ? new Set(props.rowHref.columns) : null,
|
|
698
|
-
)
|
|
699
|
-
|
|
700
638
|
const { t } = useTranslation()
|
|
701
639
|
const config = useComponentsConfig()
|
|
702
640
|
|
|
@@ -720,10 +658,6 @@ const dataUrl = computed(() =>
|
|
|
720
658
|
const sort = ref<SortConfig | null>(null)
|
|
721
659
|
const filters = ref<Record<string, ColumnFilters>>({})
|
|
722
660
|
|
|
723
|
-
if (props.initialFilters) {
|
|
724
|
-
filters.value = { ...props.initialFilters }
|
|
725
|
-
}
|
|
726
|
-
|
|
727
661
|
const PAGE_SIZE = 50
|
|
728
662
|
|
|
729
663
|
const dataQuery = computed(() => {
|
|
@@ -754,10 +688,6 @@ const dataQuery = computed(() => {
|
|
|
754
688
|
q[`${col}__isnotnull`] = ''
|
|
755
689
|
}
|
|
756
690
|
}
|
|
757
|
-
if (props.globalSearch && profileData.value?.profile) {
|
|
758
|
-
const conditions = buildGlobalSearchConditions(allColumns.value, getColumnType, props.globalSearch)
|
|
759
|
-
q.or = '(' + conditions.join(',') + ')'
|
|
760
|
-
}
|
|
761
691
|
return q
|
|
762
692
|
})
|
|
763
693
|
|
|
@@ -1008,7 +938,6 @@ onUnmounted(() => {
|
|
|
1008
938
|
const activeCell = ref<CellInfo | null>(null)
|
|
1009
939
|
|
|
1010
940
|
function onCellClick(col: string, value: unknown, event: MouseEvent) {
|
|
1011
|
-
if (props.disablePopover) return
|
|
1012
941
|
const el = (event.target as HTMLElement).closest('[data-cell]') as HTMLElement | null
|
|
1013
942
|
if (!el) return
|
|
1014
943
|
if (activeCell.value && activeCell.value.element === el) {
|
|
@@ -1032,12 +961,7 @@ const activeFilters = computed<ActiveFilter[]>(() => {
|
|
|
1032
961
|
parts.push(`= ${filter.in.join(', ')}`)
|
|
1033
962
|
}
|
|
1034
963
|
if (filter.exact != null) {
|
|
1035
|
-
|
|
1036
|
-
parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
|
|
1037
|
-
}
|
|
1038
|
-
else {
|
|
1039
|
-
parts.push(`= ${filter.exact}`)
|
|
1040
|
-
}
|
|
964
|
+
parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
|
|
1041
965
|
}
|
|
1042
966
|
if (filter.contains) {
|
|
1043
967
|
parts.push(`${t('contient')} "${filter.contains}"`)
|
|
@@ -154,14 +154,14 @@
|
|
|
154
154
|
|
|
155
155
|
<!-- Number range -->
|
|
156
156
|
<form
|
|
157
|
-
v-if="
|
|
157
|
+
v-if="columnType === 'number' && columnProfile"
|
|
158
158
|
class="px-3 py-2 border-b border-black/10 space-y-2"
|
|
159
159
|
@submit.prevent="applyRange"
|
|
160
160
|
>
|
|
161
161
|
<div class="flex items-center gap-2 text-xs text-gray-plain">
|
|
162
|
-
<span class="tabular-nums">{{
|
|
162
|
+
<span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
|
|
163
163
|
<span class="text-gray-medium">—</span>
|
|
164
|
-
<span class="tabular-nums">{{
|
|
164
|
+
<span class="tabular-nums">{{ formatNumber(profileMax) }}</span>
|
|
165
165
|
</div>
|
|
166
166
|
<div class="flex items-center gap-2">
|
|
167
167
|
<input
|
|
@@ -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
|
}
|
|
@@ -62,13 +62,13 @@ export type TabularTopValue = {
|
|
|
62
62
|
count: number
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
|
|
65
|
+
export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
|
|
66
66
|
|
|
67
67
|
export type ColumnFilters = {
|
|
68
68
|
in?: string[]
|
|
69
69
|
exact?: string
|
|
70
|
-
min?: number
|
|
71
|
-
max?: number
|
|
70
|
+
min?: number
|
|
71
|
+
max?: number
|
|
72
72
|
contains?: string
|
|
73
73
|
null?: 'only' | 'exclude'
|
|
74
74
|
}
|
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
|
+
}
|
package/src/functions/tabular.ts
CHANGED
|
@@ -42,13 +42,12 @@ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, Type
|
|
|
42
42
|
text: { icon: RiText, label: t('Texte') },
|
|
43
43
|
date: { icon: RiCalendarLine, label: t('Date') },
|
|
44
44
|
boolean: { icon: RiCheckboxLine, label: t('Booléen') },
|
|
45
|
-
year: { icon: RiCalendarLine, label: t('Année') },
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
|
|
50
|
-
if (colInfo.format === 'year') return 'year'
|
|
51
49
|
if (['int', 'float'].includes(colInfo.python_type)) return 'number'
|
|
50
|
+
if (colInfo.format === 'year') return 'date'
|
|
52
51
|
if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
|
|
53
52
|
if (colInfo.python_type === 'bool') return 'boolean'
|
|
54
53
|
if (isCategorical) return 'categorical'
|
|
@@ -159,30 +158,6 @@ export function useFormatTabular() {
|
|
|
159
158
|
const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
|
|
160
159
|
const FALSY_VALUES = ['false', '0', 'non', 'no']
|
|
161
160
|
|
|
162
|
-
/**
|
|
163
|
-
* Builds the OR conditions for global search across all columns.
|
|
164
|
-
* Text and categorical columns get a `__contains` filter; number columns
|
|
165
|
-
* get a `__exact` filter only when the search value is numeric.
|
|
166
|
-
* Date and boolean columns are excluded.
|
|
167
|
-
*/
|
|
168
|
-
export function buildGlobalSearchConditions(
|
|
169
|
-
allColumns: string[],
|
|
170
|
-
getColumnType: (col: string) => ColumnType,
|
|
171
|
-
searchValue: string,
|
|
172
|
-
): string[] {
|
|
173
|
-
const conditions: string[] = []
|
|
174
|
-
for (const col of allColumns) {
|
|
175
|
-
const type = getColumnType(col)
|
|
176
|
-
if (type === 'text' || type === 'categorical') {
|
|
177
|
-
conditions.push(col + '__contains.' + encodeURIComponent(searchValue))
|
|
178
|
-
}
|
|
179
|
-
else if (type === 'number' && searchValue.length > 0 && isFinite(Number(searchValue))) {
|
|
180
|
-
conditions.push(col + '__exact.' + encodeURIComponent(searchValue))
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return conditions
|
|
184
|
-
}
|
|
185
|
-
|
|
186
161
|
export function isTruthy(value: unknown): boolean {
|
|
187
162
|
if (typeof value === 'boolean') return value
|
|
188
163
|
if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
|
|
@@ -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>>
|