Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
634ab93
fix(resources): fix wrong checks and refactor to provide a single
nicolaskempf57 Feb 24, 2026
634899d
fix: condition
nicolaskempf57 Feb 24, 2026
7f81881
refac: naming
nicolaskempf57 Feb 24, 2026
114c3e4
wip: add chart visualization types, viewer and configurator
nicolaskempf57 Feb 20, 2026
3f4078e
feat(visualizations): configurator wip
nicolaskempf57 Feb 23, 2026
831e4cf
feat(visualizations): load columns
nicolaskempf57 Feb 23, 2026
d84c3ee
fix(datasets): fix card description
nicolaskempf57 Feb 24, 2026
1013bc2
feat(visualizations): search resources
nicolaskempf57 Feb 24, 2026
9f70092
feat: add hasTabularData
nicolaskempf57 Feb 24, 2026
66eff7e
refac: composable changes
nicolaskempf57 Feb 24, 2026
315de1e
feat: select resource and show series
nicolaskempf57 Feb 24, 2026
6881400
feat(visualizations): use tabular aggregate
nicolaskempf57 Feb 26, 2026
2c74604
feat(visualizations): fix tooltip formatting
nicolaskempf57 Feb 26, 2026
8006466
feat(visualizations): show axis formatter without unit
nicolaskempf57 Feb 26, 2026
8467303
feat(visualizations): allow no aggregate
nicolaskempf57 Feb 26, 2026
c243f44
feat(visualizations): sort transformer wip
nicolaskempf57 Feb 26, 2026
a74773a
feat(visualizations): sort
nicolaskempf57 Feb 27, 2026
ebea8c7
feat: add save chart
nicolaskempf57 Mar 2, 2026
3f01507
refactor(chart-configurator): use savedResources and add chart loading
nicolaskempf57 Mar 4, 2026
de61eb9
refactor(chart-configurator): separate title/description refs and add…
nicolaskempf57 Mar 5, 2026
ce6cc1f
feat(components): export useDebouncedRef
nicolaskempf57 Mar 5, 2026
d3f5586
refactor(chart-configurator): restore satisfies clause
nicolaskempf57 Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions components/Datasets/FileEditModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@
</template>

<script setup lang="ts">
import { BannerAction, BrandedButton, isCommunityResource } from '@datagouv/components-next'
import { BannerAction, BrandedButton, isCommunityResource, useHasTabularData } from '@datagouv/components-next'
import type { Dataset, DatasetV2, Resource } from '@datagouv/components-next'
import { cloneDeep } from 'lodash-es'
import { RiDeleteBin6Line, RiPencilLine } from '@remixicon/vue'
Expand Down Expand Up @@ -228,10 +228,12 @@ const resourceForm = ref(cloneDeep(props.resource))
const open = ref(false)
const hasFileChanged = ref(false)

// Use the composable for tabular data detection
const hasTabularData = useHasTabularData()

// Check if resource has tabular API
const hasTabularApi = computed(() => {
return props.resource.resource?.extras?.['analysis:parsing:parsing_table']
&& !props.resource.resource?.extras?.['analysis:parsing:error']
return props.resource.resource ? hasTabularData(props.resource.resource) : false
})

// Watch for file changes
Expand Down
2 changes: 1 addition & 1 deletion components/dataset/card-lg.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<DatasetCard
:dataset
:style
:show-description
:show-description-short="showDescription"
:dataset-url="dataset.page"
:organization-url="dataset.organization?.page"
/>
Expand Down
4 changes: 3 additions & 1 deletion datagouv-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@
},
"dependencies": {
"@floating-ui/vue": "^1.1.8",
"@types/leaflet": "^1.9.17",
"@headlessui/vue": "^1.7.23",
"@remixicon/vue": "^4.5.0",
"@types/hast": "^3.0.4",
"@types/leaflet": "^1.9.17",
"@vueuse/core": "^13.1.0",
"@vueuse/router": "^13.1.0",
"chart.js": "^4.4.8",
"dompurify": "^3.2.5",
"echarts": "^6.0.0",
"geopf-extensions-openlayers": "^1.0.0-beta.5",
"leaflet": "^1.9.4",
"maplibre-gl": "^5.6.2",
Expand All @@ -70,6 +71,7 @@
"unist-util-visit": "^5.0.0",
"vue": "^3.5.13",
"vue-content-loader": "^2.0.1",
"vue-echarts": "^8.0.1",
"vue-router": "^4.5.0",
"vue-sonner": "^2.0.9",
"vue3-json-viewer": "^2.4.1",
Expand Down
152 changes: 152 additions & 0 deletions datagouv-components/src/components/Chart/ChartViewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<template>
<VChart
class="w-full min-h-96"
:option="echartsOption"
autoresize
/>
</template>

<script setup lang="ts">
import { format, use, type ComposeOption } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, BarChart, type BarSeriesOption, type LineSeriesOption } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { computed } from 'vue'
import { summarize } from '../../functions/helpers'
import type { Chart, DataSeries, XAxis, YAxis, ChartForm } from '../../types/visualizations'

use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])

const props = defineProps<{
chart: Chart | ChartForm
series: {
data: Record<string, Array<Record<string, unknown>>>
columns: Record<string, Array<string>>
}
}>()

function mapSeriesType(type: DataSeries['type']): 'line' | 'bar' {
return (type ?? 'line') === 'histogram' ? 'bar' : 'line'
}

function mapXAxisType(xAxis?: XAxis | Partial<XAxis>): 'category' | 'value' {
if (!xAxis) return 'category'
return (xAxis.type ?? 'discrete') === 'continuous' ? 'value' : 'category'
}

function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
return (value: number) => {
const v = summarize(value)
if (!yAxis.unit) return v
if (yAxis.unit_position === 'prefix') return `${yAxis.unit} ${v}`
return `${v} ${yAxis.unit}`
}
}

const echartsOption = computed(() => {
const seriesCount = props.chart.series.length
if (!props.chart.series || seriesCount === 0) return

// Create series configuration with data mapping
const seriesData = props.chart.series.map((s) => {
const xColumn = s.column_x_name_override ?? props.chart.x_axis.column_x
const yColumn = s.aggregate_y ? `${s.column_y}__${s.aggregate_y}` : s.column_y
const resourceId = s.resource_id
const seriesType = s.type

if (!xColumn || !yColumn || !resourceId || !seriesType || !props.series.data[resourceId] || !props.series.columns[resourceId]) {
return null
}

// Sort data before passing to ECharts to avoid transform issues
const sortedData = [...props.series.data[resourceId]]
const sortBy = props.chart.x_axis.sort_x_by
const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'

if (sortBy && sortDirection && props.chart.x_axis.column_x) {
const sortKey = sortBy === 'axis_x' ? xColumn : yColumn
sortedData.sort((a, b) => {
const valA = a[sortKey] as number
const valB = b[sortKey] as number
if (valA < valB) return sortDirection === 'asc' ? -1 : 1
if (valA > valB) return sortDirection === 'asc' ? 1 : -1
return 0
})
}

return {
series: {
type: mapSeriesType(seriesType),
dimensions: s.aggregate_y ? [xColumn, yColumn] : props.series.columns[resourceId],
name: s.column_y,
encode: {
x: xColumn,
y: yColumn,
},
} as LineSeriesOption | BarSeriesOption,
data: {
source: sortedData,
dimensions: s.aggregate_y ? [xColumn, yColumn] : props.series.columns[resourceId],
},
}
}).filter(Boolean).reduce((acc: { series: Array<LineSeriesOption | BarSeriesOption>, data: Array<Record<string, unknown>> }, curr) => {
if (curr) {
acc.series.push(curr.series)
acc.data.push(curr.data)
}
return acc
}, {
series: [],
data: [],
})

return {
dataset: [...seriesData.data],
title: {
text: props.chart.title,
left: 'center',
},
tooltip: {
trigger: 'axis' as const,
formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
let tooltip = ''
for (const param of params) {
const keys = Object.keys(param.value)
const col = keys.find(key => key.startsWith(param.seriesName))!
const formatter = new Intl.NumberFormat('fr-FR')
tooltip += `${format.encodeHTML(param.axisValueLabel)}: <strong>${formatter.format(Number(param.value[col]))}</strong><br>`
}
return tooltip
},
},
legend: {
bottom: 0,
},
grid: {
top: 60,
bottom: 40,
left: 20,
right: 20,
containLabel: true,
},
xAxis: {
type: mapXAxisType(props.chart.x_axis),
name: (props.chart.x_axis as XAxis).column_x,
},
yAxis: {
type: 'value' as const,
name: props.chart.y_axis.label ?? undefined,
min: props.chart.y_axis.min ?? undefined,
max: props.chart.y_axis.max ?? undefined,
axisLabel: {
formatter: buildYAxisFormatter(props.chart.y_axis),
},
},
series: seriesData.series,
} satisfies ComposeOption<
| BarSeriesOption
| LineSeriesOption
>
})
</script>
Loading
Loading