@geo2france/api-dashboard 1.6.2 → 1.8.0

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/README.MD CHANGED
@@ -29,7 +29,15 @@ Les composants sont actuellement utilisés pour le [tableau de bord de l'Odema](
29
29
 
30
30
  ## Installation
31
31
 
32
- `npm i @geo2france/api-dashboard`
32
+ Pour créer un nouveau tableau de bord :
33
+
34
+ ```sh
35
+ npm init @geo2france/api-dashboard@latest nom-du-projet
36
+ cd nom-du-projet && npm install
37
+
38
+ # Lancer le serveur de développement
39
+ npm run dev
40
+ ```
33
41
 
34
42
  Consulter la [documentation](https://geo2france.github.io/api-dashboard/) du projet.
35
43
 
@@ -5,6 +5,7 @@ interface IChartPieProps {
5
5
  unit?: string;
6
6
  title?: string;
7
7
  donut?: boolean;
8
+ other?: number | null;
8
9
  }
9
10
  export declare const ChartPie: React.FC<IChartPieProps>;
10
11
  export {};
@@ -5,7 +5,8 @@ import { ChartBlockContext } from "../DashboardPage/Block";
5
5
  import { usePalette } from "../Palette/Palette";
6
6
  import { from, op } from "arquero";
7
7
  import { ChartEcharts } from "./ChartEcharts";
8
- export const ChartPie = ({ dataset: dataset_id, nameKey, dataKey, unit, title, donut = false }) => {
8
+ import { merge_others } from "../..";
9
+ export const ChartPie = ({ dataset: dataset_id, nameKey, dataKey, unit, title, donut = false, other = 5 }) => {
9
10
  const dataset = useDataset(dataset_id);
10
11
  const blockConfig = useContext(ChartBlockContext);
11
12
  const data = dataset?.data;
@@ -14,10 +15,17 @@ export const ChartPie = ({ dataset: dataset_id, nameKey, dataKey, unit, title, d
14
15
  dataExport: data
15
16
  };
16
17
  useEffect(() => blockConfig?.setConfig(block_config), [data]);
17
- const chart_data = data && from(data).groupby(nameKey).rollup({ value: op.sum(dataKey) }).objects().map((d) => ({
18
- name: d[nameKey],
19
- value: d.value,
20
- }));
18
+ const chart_data1 = data && data.length > 0
19
+ ? from(data)
20
+ .groupby(nameKey)
21
+ .rollup({ value: op.sum(dataKey) })
22
+ .objects()
23
+ .map((d) => ({
24
+ name: d[nameKey],
25
+ value: d.value,
26
+ }))
27
+ : [];
28
+ const chart_data = merge_others({ dataset: chart_data1 || [], min: other || -1 });
21
29
  const option = {
22
30
  color: usePalette({ nColors: chart_data?.length }),
23
31
  xAxis: { show: false }, yAxis: { show: false },
@@ -4,7 +4,6 @@
4
4
  interface IYearSerieProps {
5
5
  dataset: string;
6
6
  title?: string;
7
- yearControl?: string;
8
7
  yearKey: string;
9
8
  valueKey: string;
10
9
  categoryKey?: string;
@@ -6,27 +6,39 @@ import { from, op } from "arquero";
6
6
  import { useDataset } from "../Dataset/hooks";
7
7
  import { usePalette } from "../Palette/Palette";
8
8
  import { ChartEcharts } from "./ChartEcharts";
9
- export const ChartYearSerie = ({ dataset: dataset_id, categoryKey, valueKey, yearKey, yearMark, stack: stack_input, type: chart_type = 'bar', yearControl = 'year' }) => {
9
+ import { useContext, useEffect } from "react";
10
+ import { ChartBlockContext } from "../DashboardPage/Block";
11
+ export const ChartYearSerie = ({ dataset: dataset_id, categoryKey, valueKey, yearKey, yearMark, stack: stack_input, title, type: chart_type = 'bar' }) => {
10
12
  const stack = stack_input || chart_type == 'line' ? false : true; // Pas de stack par défaut pour le type line
11
13
  const dataset = useDataset(dataset_id);
12
14
  const data = dataset?.data;
13
- const chart_data = categoryKey ? data && from(data).groupby(yearKey, categoryKey) //Somme par année et categorykey
14
- .rollup({ [valueKey]: op.sum(valueKey) })
15
- .groupby(yearKey).orderby(yearKey).objects()
16
- :
17
- data && from(data).groupby(yearKey) //Somme par année seulement
18
- .rollup({ [valueKey]: op.sum(valueKey) }).orderby(yearKey)
19
- .objects();
20
- const distinct_cat = categoryKey ?
21
- data && from(data).rollup({ cat: op.array_agg_distinct(categoryKey) }).object().cat
22
- :
23
- [valueKey];
15
+ let chart_data = [];
16
+ let distinct_cat = [];
17
+ const blockConfig = useContext(ChartBlockContext); //TODO : créer un hook pour simplifier la config du block
18
+ const block_config = {
19
+ title: title,
20
+ dataExport: data
21
+ };
22
+ useEffect(() => blockConfig?.setConfig(block_config), [title, data]);
23
+ if (data && data.length > 0) {
24
+ const grouped_data = categoryKey ? from(data).groupby(yearKey, categoryKey) //Somme par année et categorykey
25
+ .rollup({ [valueKey]: op.sum(valueKey) })
26
+ .groupby(yearKey).orderby(yearKey)
27
+ :
28
+ from(data).groupby(yearKey) //Somme par année seulement
29
+ .rollup({ [valueKey]: op.sum(valueKey) }).orderby(yearKey);
30
+ const all_years = from(data).groupby(yearKey).rollup({ [yearKey]: op.any(yearKey) });
31
+ const all_cats = categoryKey ? (from(data).groupby(categoryKey).rollup({ [categoryKey]: op.any(categoryKey) })) : from([{ 'cat': valueKey }]);
32
+ const full = all_years.cross(all_cats); // Contient chaque annee x catégorie (pour éviter les trous)
33
+ distinct_cat = all_cats.array(categoryKey || 'cat'); // Pour générer chaque serie
34
+ chart_data = full.join_left(grouped_data).objects();
35
+ }
24
36
  const COLORS = usePalette({ nColors: distinct_cat?.length }) || [];
25
- const series = distinct_cat ? distinct_cat.map((cat, idx) => ({
37
+ const series = distinct_cat.map((cat, idx) => ({
26
38
  name: cat,
27
39
  type: chart_type === 'area' ? 'line' : chart_type,
28
- data: categoryKey ? chart_data?.filter((row) => row[categoryKey] === cat).map((row) => ([String(row[yearKey]), row[valueKey]]))
29
- : chart_data?.map((row) => ([String(row[yearKey]), row[valueKey]])),
40
+ data: categoryKey ? chart_data?.filter((row) => row[categoryKey] === cat).map((row) => ([String(row[yearKey]), row[valueKey] || 0]))
41
+ : chart_data?.map((row) => ([String(row[yearKey]), row[valueKey] || 0])),
30
42
  itemStyle: {
31
43
  color: COLORS && COLORS[idx % COLORS.length],
32
44
  },
@@ -38,7 +50,7 @@ export const ChartYearSerie = ({ dataset: dataset_id, categoryKey, valueKey, yea
38
50
  { xAxis: String(yearMark) }
39
51
  ]
40
52
  } : undefined
41
- })) : [];
53
+ }));
42
54
  function tooltipFormatter(params) {
43
55
  if (!params || params.length === 0)
44
56
  return '';
@@ -15,6 +15,7 @@ const Control = ({ children, style = {} }) => {
15
15
  backgroundColor: "#fff",
16
16
  height: "auto",
17
17
  width: "100%",
18
+ borderBottom: "1px solid #ccc",
18
19
  ...style,
19
20
  }, children: children }));
20
21
  };
@@ -39,5 +39,6 @@ interface IDSLDashboardPageProps {
39
39
  children: React.ReactNode;
40
40
  name?: string;
41
41
  columns?: number;
42
+ debug?: boolean;
42
43
  }
43
44
  export declare const DSL_DashboardPage: React.FC<IDSLDashboardPageProps>;
@@ -40,7 +40,7 @@ export default DashboardPage;
40
40
  export const DatasetContext = createContext({});
41
41
  export const DatasetRegistryContext = createContext(() => { }); // A modifier, utiliser un seul context
42
42
  export const ControlContext = createContext(undefined);
43
- export const DSL_DashboardPage = ({ name = 'Tableau de bord', columns = 2, children }) => {
43
+ export const DSL_DashboardPage = ({ name = 'Tableau de bord', columns = 2, children, debug = false }) => {
44
44
  const [datasets, setdatasets] = useState({});
45
45
  const [palette, setPalette] = useState(DEFAULT_PALETTE);
46
46
  //const allDatasetLoaded = Object.values(datasets).every(d => !d.isFetching);
@@ -68,6 +68,9 @@ export const DSL_DashboardPage = ({ name = 'Tableau de bord', columns = 2, child
68
68
  const visible_components = childrenArray.filter((c) => c && getComponentKind(c) == 'other');
69
69
  const logic_components = childrenArray.filter((c) => getComponentKind(c) == 'logical');
70
70
  const control_components = childrenArray.filter((c) => getComponentKind(c) == 'control');
71
+ if (debug && !logic_components.some((c) => typeof c.type !== "string" && c.type.name === Debug.name)) {
72
+ logic_components.push(_jsx(Debug, {}, "debug_property"));
73
+ }
71
74
  return (_jsxs(_Fragment, { children: [_jsx(Helmet, { children: _jsx("title", { children: name }) }), _jsx(DatasetRegistryContext.Provider, { value: pushDataset, children: _jsx(DatasetContext.Provider, { value: datasets, children: _jsxs(PaletteContext.Provider, { value: { palette, setPalette }, children: [control_components.length > 0 && _jsx(Header, { style: {
72
75
  padding: 12,
73
76
  position: "sticky",
@@ -41,6 +41,6 @@ const DashboardApp = ({ routes, theme, logo, brands, title, subtitle }) => {
41
41
  ...c
42
42
  }));
43
43
  };
44
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ConfigProvider, { theme: theme || default_theme /* Merger plutôt ?*/, children: _jsx(HelmetProvider, { children: _jsx(AppContext.Provider, { value: context_values, children: _jsx(ControlContext.Provider, { value: { values: controls, pushValue: pushControl }, children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { children: [_jsxs(Layout, { children: [_jsx(DashboardSider, { route_config: routes }), _jsx(Content, { style: { width: "85%" }, children: _jsx(Outlet, {}) })] }), _jsx(DasbhoardFooter, { brands: brands })] }), children: [generateRoutes(routes), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
44
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ConfigProvider, { theme: theme || default_theme /* Merger plutôt ?*/, children: _jsx(HelmetProvider, { children: _jsx(AppContext.Provider, { value: context_values, children: _jsx(ControlContext.Provider, { value: { values: controls, pushValue: pushControl }, children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes }), _jsxs(Layout, { children: [_jsx(Content, { style: { width: "100%" }, children: _jsx(Outlet, {}) }), _jsx(DasbhoardFooter, { brands: brands })] })] }), children: [generateRoutes(routes), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
45
45
  };
46
46
  export default DashboardApp;
@@ -1,9 +1,9 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Layout, Typography } from "antd";
2
+ import { Button, Layout, Typography } from "antd";
3
3
  import { useContext, useState } from "react";
4
4
  import { UpOutlined, DownOutlined } from "@ant-design/icons";
5
5
  import { AppContext } from "./DashboardApp";
6
- const { Text, Link } = Typography;
6
+ const { Text } = Typography;
7
7
  export const DasbhoardFooter = ({ brands }) => {
8
8
  const [isCollapsed, setIsCollapsed] = useState(window.innerWidth < 768 ? true : false);
9
9
  const toggleCollapse = () => {
@@ -11,7 +11,7 @@ export const DasbhoardFooter = ({ brands }) => {
11
11
  };
12
12
  const app_context = useContext(AppContext);
13
13
  const style_img = {
14
- height: "60px",
14
+ maxHeight: "60px",
15
15
  marginRight: "20px",
16
16
  };
17
17
  return (_jsxs(Layout.Footer, { style: {
@@ -22,26 +22,16 @@ export const DasbhoardFooter = ({ brands }) => {
22
22
  position: "sticky",
23
23
  right: "0",
24
24
  width: "100%",
25
- height: isCollapsed ? "0px" : "150px",
25
+ padding: 2,
26
+ height: isCollapsed ? "40px" : "80px",
26
27
  transition: "height 0.5s ease-in-out",
27
28
  overflow: "hidden",
28
29
  borderTop: "1px solid #ccc",
29
30
  zIndex: 600, // maplibre top zIndex if 500
30
- }, children: [isCollapsed && (_jsx("div", { style: {
31
- color: "#000",
32
- fontSize: "14px",
33
- marginTop: -15,
34
- }, children: _jsx(Link, { href: "/", target: "_blank", children: _jsxs(Text, { children: [app_context?.title, " - ", app_context?.subtitle] }) }) })), _jsx("div", { style: { display: isCollapsed ? "none" : "block", padding: "10px 0" }, children: brands?.map((p) => (_jsx("a", { href: p.url, children: _jsx("img", { style: style_img, src: p.logo, alt: p.name }) }, p.name))) }), _jsx("div", { style: {
31
+ }, children: [isCollapsed && (_jsxs(Text, { type: "secondary", children: [app_context?.title, " - ", app_context?.subtitle] })), _jsx("div", { style: { display: isCollapsed ? "none" : "block", padding: "10px 0" }, children: brands?.map((p) => (_jsx("a", { href: p.url, children: _jsx("img", { style: style_img, src: p.logo, alt: p.name }) }, p.name))) }), _jsx(Button, { style: {
35
32
  position: "absolute",
36
33
  bottom: "5px",
37
34
  right: "10px",
38
- cursor: "pointer",
39
35
  zIndex: 1001,
40
- backgroundColor: "#dead8f",
41
- padding: "5px",
42
- display: "flex",
43
- alignItems: "center",
44
- justifyContent: "center",
45
- borderRadius: "4px",
46
- }, onClick: toggleCollapse, children: isCollapsed ? (_jsx(UpOutlined, { style: { fontSize: "16px", color: "#fff" } })) : (_jsx(DownOutlined, { style: { fontSize: "16px", color: "#fff" } })) })] }));
36
+ }, type: "primary", onClick: toggleCollapse, "aria-label": isCollapsed ? "Développer le footer" : "Réduire le footer", children: isCollapsed ? _jsx(UpOutlined, {}) : _jsx(DownOutlined, {}) })] }));
47
37
  };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useContext, useState } from "react";
3
3
  import { Layout, Menu, theme, Row, Col, Button, Divider } from "antd";
4
4
  import { NavLink, useLocation } from "react-router-dom";
@@ -23,26 +23,30 @@ const DashboardSider = ({ style, logo, route_config }) => {
23
23
  setCollapsed(!collapsed);
24
24
  };
25
25
  const siderStyle = {
26
+ overflow: 'auto',
26
27
  height: "100vh",
27
28
  backgroundColor: token.colorBgContainer,
28
29
  zIndex: 2,
30
+ position: 'sticky',
31
+ top: '0',
32
+ borderRight: "1px solid #ccc",
29
33
  ...style
30
34
  };
31
- return (_jsx(_Fragment, { children: _jsx(Layout.Sider, { theme: "light", collapsible: true, collapsedWidth: isMobile ? 40 : 80, collapsed: collapsed, onCollapse: toggleCollapsed, style: siderStyle, width: isMobile ? '80%' : 220, trigger: null, children: _jsxs(Row, { justify: "center", children: [_jsx(Col, { span: 24, children: _jsxs("div", { style: {
32
- margin: 4,
33
- display: "flex",
34
- justifyContent: "center",
35
- alignItems: "center",
36
- backgroundColor: token.colorBgElevated,
37
- }, children: [_jsx(NavLink, { to: "", style: {
38
- display: collapsed ? 'none' : undefined,
39
- marginTop: 8, marginLeft: 8
40
- }, children: _jsx("img", { style: style_img, src: appLogo, alt: title }) }), _jsx(Divider, { style: { display: collapsed ? 'none' : undefined }, type: "vertical" }), _jsx(Button, { type: "text", onClick: () => setCollapsed(!collapsed), icon: collapsed ? _jsx(MdOutlineKeyboardDoubleArrowRight, {}) : _jsx(MdOutlineKeyboardDoubleArrowLeft, {}), style: {
41
- fontSize: '28px',
42
- width: 32,
43
- height: 32,
44
- //backgroundColor: token.colorFillSecondary,
45
- marginTop: 8
46
- } })] }) }), _jsx(Col, { span: 24, children: _jsx(Menu, { items: route_config && generateMenuItems(route_config), selectedKeys: [selectedKey], mode: "inline", style: { marginTop: "20px", width: "100%" } }) })] }) }) }));
35
+ return (_jsx(Layout.Sider, { theme: "light", collapsible: true, collapsedWidth: isMobile ? 40 : 80, collapsed: collapsed, onCollapse: toggleCollapsed, style: siderStyle, width: isMobile ? '80%' : 220, trigger: null, children: _jsxs(Row, { justify: "center", children: [_jsx(Col, { span: 24, children: _jsxs("div", { style: {
36
+ margin: 4,
37
+ display: "flex",
38
+ justifyContent: "center",
39
+ alignItems: "center",
40
+ backgroundColor: token.colorBgElevated,
41
+ }, children: [_jsx(NavLink, { to: "", style: {
42
+ display: collapsed ? 'none' : undefined,
43
+ marginTop: 8, marginLeft: 8
44
+ }, children: _jsx("img", { style: style_img, src: appLogo, alt: title }) }), _jsx(Divider, { style: { display: collapsed ? 'none' : undefined }, type: "vertical" }), _jsx(Button, { type: "text", onClick: () => setCollapsed(!collapsed), icon: collapsed ? _jsx(MdOutlineKeyboardDoubleArrowRight, {}) : _jsx(MdOutlineKeyboardDoubleArrowLeft, {}), style: {
45
+ fontSize: '28px',
46
+ width: 32,
47
+ height: 32,
48
+ //backgroundColor: token.colorFillSecondary,
49
+ marginTop: 8
50
+ } })] }) }), _jsx(Col, { span: 24, children: _jsx(Menu, { items: route_config && generateMenuItems(route_config), selectedKeys: [selectedKey], mode: "inline", style: { marginTop: "20px", width: "100%" } }) })] }) }));
47
51
  };
48
52
  export default DashboardSider;
@@ -15,7 +15,6 @@ export const generateFilter = (filters) => {
15
15
  case "endswith":
16
16
  return `*${filter.value}`;
17
17
  case "in":
18
- console.log(filter.value);
19
18
  return filter.value.join(' ');
20
19
  }
21
20
  })();
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { useChartData, useDashboardElement, useNoData } from "./components/Dashb
6
6
  export { useMapControl } from "./utils/useMapControl";
7
7
  export { BaseRecordToGeojsonPoint } from "./utils/baserecordtogeojsonpoint";
8
8
  export { cardStyles } from "./utils/cardStyles";
9
+ export { merge_others } from "./utils/merge_others";
9
10
  import KeyFigure from "./components/KeyFigure/KeyFigure";
10
11
  import LoadingContainer from "./components/LoadingContainer/LoadingContainer";
11
12
  import FlipCard from "./components/FlipCard/FlipCard";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { useMapControl } from "./utils/useMapControl";
8
8
  // Helpers
9
9
  export { BaseRecordToGeojsonPoint } from "./utils/baserecordtogeojsonpoint";
10
10
  export { cardStyles } from "./utils/cardStyles";
11
+ export { merge_others } from "./utils/merge_others";
11
12
  // Components
12
13
  import KeyFigure from "./components/KeyFigure/KeyFigure";
13
14
  import LoadingContainer from "./components/LoadingContainer/LoadingContainer";
@@ -0,0 +1,8 @@
1
+ import { SimpleRecord } from "../types";
2
+ interface MergeOthersOpts {
3
+ dataset: SimpleRecord[];
4
+ min: number;
5
+ other_cat_name?: string;
6
+ }
7
+ export declare const merge_others: ({ dataset, min, other_cat_name }: MergeOthersOpts) => SimpleRecord[];
8
+ export {};
@@ -0,0 +1,14 @@
1
+ import { from, op } from "arquero";
2
+ export const merge_others = ({ dataset, min, other_cat_name = 'Autres' }) => {
3
+ const total = dataset.reduce((acc, d) => acc + d.value, 0);
4
+ const small = dataset.filter(d => (d.value / total) * 100 < min);
5
+ const large = dataset.filter(d => (d.value / total) * 100 >= min);
6
+ if (small.length > 1) {
7
+ const others_row = from(small).rollup({ value: op.sum('value') }).objects().map((d) => ({
8
+ name: other_cat_name,
9
+ value: d.value
10
+ }));
11
+ return [...large, ...others_row];
12
+ }
13
+ return dataset;
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geo2france/api-dashboard",
3
- "version": "1.6.2",
3
+ "version": "1.8.0",
4
4
  "private": false,
5
5
  "description": "Build dashboards with JSX/TSX",
6
6
  "main": "dist/index.js",