@opengis/bi 1.0.11 → 1.0.12

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 (88) hide show
  1. package/dist/assets/charts/bar.png +0 -0
  2. package/dist/assets/charts/funnel.png +0 -0
  3. package/dist/assets/charts/no_data.jpg +0 -0
  4. package/dist/assets/charts/number.png +0 -0
  5. package/dist/assets/charts/pie.png +0 -0
  6. package/dist/assets/charts/progress.png +0 -0
  7. package/dist/assets/charts/stat.png +0 -0
  8. package/dist/assets/images/bar.png +0 -0
  9. package/dist/assets/images/funnel.png +0 -0
  10. package/dist/assets/images/no_data.jpg +0 -0
  11. package/dist/assets/images/number.png +0 -0
  12. package/dist/assets/images/pie.png +0 -0
  13. package/dist/assets/images/progress.png +0 -0
  14. package/dist/assets/images/stat.png +0 -0
  15. package/dist/bi.js +1 -1
  16. package/dist/bi.umd.cjs +723 -113
  17. package/dist/{import-file-D6RYWvi_.js → import-file-Db7C78fp.js} +43157 -43204
  18. package/dist/style.css +1 -1
  19. package/dist/vs-calendar-Ddl6WRL3.js +96 -0
  20. package/dist/vs-funnel-bar-GbisTylf.js +92 -0
  21. package/dist/vs-heatmap-CPiim0yg.js +83 -0
  22. package/dist/vs-map-DuBKvlTI.js +19422 -0
  23. package/dist/vs-number-CR1H0JTM.js +39 -0
  24. package/dist/{vs-text-UyIWGqQO.js → vs-text-C3RkizPQ.js} +60 -60
  25. package/package.json +7 -5
  26. package/server/routes/dashboard/controllers/dashboard.js +20 -1
  27. package/server/routes/data/controllers/data.js +31 -19
  28. package/server/routes/data/controllers/util/chartSQL.js +8 -7
  29. package/server/routes/data/controllers/util/normalizeData.js +10 -4
  30. package/server/routes/edit/controllers/dashboard.add.js +5 -1
  31. package/server/routes/edit/controllers/dashboard.edit.js +5 -1
  32. package/server/routes/edit/controllers/widget.add.js +23 -7
  33. package/server/routes/edit/controllers/widget.del.js +1 -1
  34. package/server/routes/edit/controllers/widget.edit.js +8 -5
  35. package/server/routes/map/controllers/cluster.js +90 -0
  36. package/server/routes/map/controllers/clusterVtile.js +144 -0
  37. package/server/routes/map/controllers/geojson.js +1 -1
  38. package/server/routes/map/controllers/map.js +63 -0
  39. package/server/routes/map/controllers/vtile.js +1 -1
  40. package/server/routes/map/index.mjs +7 -4
  41. package/server/templates/cls/demo.parcel.object_type.json +12 -0
  42. package/server/templates/dashboard/demo/funnel.yml +18 -0
  43. package/server/templates/dashboard/demo/heatmap.yml +18 -0
  44. package/server/templates/dashboard/demo/index.yml +58 -0
  45. package/server/templates/dashboard/demo/line.yml +19 -0
  46. package/server/templates/dashboard/demo/map.yml +13 -0
  47. package/server/templates/dashboard/demo/pivot.yml +18 -0
  48. package/server/templates/dashboard/demo/progress.yml +15 -0
  49. package/server/templates/dashboard/demo/quarterly_revenue.yml +17 -0
  50. package/server/templates/dashboard/demo/quarterly_revenue_by_product_line.yml +19 -0
  51. package/server/templates/dashboard/demo/stat.yml +15 -0
  52. package/server/templates/dashboard/demo/total_products_sold.yml +9 -0
  53. package/server/templates/dashboard/demo/total_products_sold_by_product_line.yml +12 -0
  54. package/server/templates/dashboard/demo/total_revenue.yml +10 -0
  55. package/server/templates/dashboard/demo/total_revenue_by_product_line.yml +20 -0
  56. package/server/templates/dashboard/demo/vehicle_sales_info.md +17 -0
  57. package/server/templates/dashboard/demo/waterfall.yml +19 -0
  58. package/server/templates/dashboard/erobota/bar_area.yml +3 -1
  59. package/server/templates/dashboard/erobota/bar_culture.yml +1 -1
  60. package/server/templates/dashboard/erobota/bar_grand.yml +3 -2
  61. package/server/templates/dashboard/erobota/count_grand.yml +1 -1
  62. package/server/templates/dashboard/erobota/index.yml +14 -12
  63. package/server/templates/dashboard/erobota/list_culture.yml +1 -1
  64. package/server/templates/dashboard/erobota/list_grant.yml +1 -1
  65. package/server/templates/dashboard/erobota/map.yml +1 -1
  66. package/server/templates/dashboard/erobota/pie_area.yml +1 -1
  67. package/server/templates/dashboard/erobota/pie_grant.yml +1 -1
  68. package/server/templates/dashboard/erobota/total_area.yml +1 -1
  69. package/server/templates/dashboard/erobota/total_grand.yml +1 -1
  70. package/server/templates/dashboard/map/index.yml +6 -0
  71. package/server/templates/dashboard/map/map.yml +13 -0
  72. package/server/templates/dashboard/map/mapCluster.yml +16 -0
  73. package/server/templates/dashboard/sales/index.yml +4 -3
  74. package/server/templates/dashboard/sales/quarterly_revenue.yml +1 -3
  75. package/server/templates/dashboard/sales/quarterly_revenue_by_product_line.yml +1 -1
  76. package/server/templates/dashboard/sales/total_products_sold.yml +1 -1
  77. package/server/templates/dashboard/sales/total_products_sold_by_product_line.yml +2 -1
  78. package/server/templates/dashboard/sales/total_revenue.yml +1 -1
  79. package/server/templates/dashboard/sales/total_revenue_by_product_line.yml +4 -1
  80. package/server/templates/dashboard/test3/quarterly_revenue.yml +1 -1
  81. package/server/templates/dashboard/test3/widget1.yml +1 -1
  82. package/server/templates/widget/calendar.yml +14 -0
  83. package/server/templates/widget/map.yml +15 -0
  84. package/server/templates/widget/mapCluster.yml +16 -0
  85. package/server/templates/widget/negative.yml +18 -0
  86. package/server/templates/widget/negative_profi_expense.yml +23 -0
  87. package/server/templates/widget/negative_type.yml +24 -0
  88. package/dist/vs-number-DKF5ptAP.js +0 -34
@@ -14,7 +14,7 @@ export default async function widgetDel({ pg = {}, params = {} }) {
14
14
  const row = await pg
15
15
  .query(
16
16
  `select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
17
- where a.name=$2 and $1 in (b.dashboard_id, b.name) order by 1,2`,
17
+ where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
18
18
  [dashboardName, widgetName]
19
19
  )
20
20
  .then((res1) => res1.rows?.[0] || {});
@@ -1,7 +1,7 @@
1
1
  import dataUpdate from '@opengis/fastify-table/crud/funcs/dataUpdate.js';
2
2
  export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, params = {} }) {
3
3
  const { widget: widgetName, name: dashboardName } = params;
4
-
4
+ const tableName = body.table_name;
5
5
  if (!widgetName || !dashboardName) {
6
6
  return {
7
7
  message: 'not enough params: dashboard and widget name',
@@ -13,12 +13,15 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
13
13
  const row = await pg
14
14
  .query(
15
15
  `select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
16
- where a.name=$2 and $1 in (b.dashboard_id, b.name) order by 1,2`,
16
+ where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
17
17
  [dashboardName, widgetName]
18
18
  )
19
19
  .then((res1) => res1.rows?.[0] || {});
20
-
20
+ const checkTable = await pg.query(`select * from bi.widget where $1 in (table_name)`, [
21
+ tableName
22
+ ]);
21
23
  const { widget_id: widgetId, dashboard_id: dashboardId } = row || {};
24
+ if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
22
25
 
23
26
  if (!widgetId) {
24
27
  return { message: `widget not found ${widgetName}`, status: 404 };
@@ -47,7 +50,7 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
47
50
  Array.isArray(bodyDashboard.panels) && bodyDashboard.panels?.length
48
51
  ? bodyDashboard.panels?.map((panel) => {
49
52
  if (panel.widget === widgetName) {
50
- panel.col = body.col;
53
+ panel.col = body.col;
51
54
  }
52
55
  return panel;
53
56
  })
@@ -56,7 +59,7 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
56
59
  Array.isArray(bodyDashboard.widgets) && bodyDashboard?.widgets?.length
57
60
  ? bodyDashboard.widgets?.map((widget) => {
58
61
  if (widget.name === widgetName) {
59
- return body;
62
+ return body;
60
63
  }
61
64
  return widget;
62
65
  })
@@ -0,0 +1,90 @@
1
+ import path from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+
4
+ import yaml from '../../dashboard/controllers/utils/yaml.js';
5
+
6
+ export default async function cluster({
7
+ pg, funcs, query = {}, log,
8
+ }) {
9
+ const {
10
+ widget, filter, dashboard, search,
11
+ } = query;
12
+
13
+ if (!widget) {
14
+ return { message: 'not enough params: widget', status: 400 };
15
+ }
16
+
17
+ const { config = {} } = funcs;
18
+
19
+ const cwd = process.cwd();
20
+ const widgetDir = dashboard ? path.join(cwd, 'server/templates/dashboard', dashboard) : path.join(cwd, 'server/templates/widget');
21
+
22
+ const filePath = dashboard ? path.join(widgetDir, (widget || 'index') + '.yml') : path.join(widgetDir, widget + '.yml');
23
+
24
+ if (!existsSync(filePath)) {
25
+ return { message: { root: config.local ? widgetDir : undefined, error: `not found`, widget }, status: 404 };
26
+ }
27
+
28
+ try {
29
+ const fileData = readFileSync(filePath, 'utf-8');
30
+ const { data = {} } = filePath.includes('.yml') ? yaml.loadSafe(fileData) : {};
31
+
32
+ if (!data?.table) {
33
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`, status: 400 };
34
+ }
35
+
36
+ const pkey = pg.pk?.[data?.table];
37
+
38
+ if (!pkey) {
39
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
40
+ }
41
+
42
+ // data param
43
+ const { table, query: where = '1=1', metrics = [], cluster, clusterTable = {} } = data;
44
+
45
+ if (!cluster) {
46
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`, status: 400 };
47
+ }
48
+
49
+ if (!metrics.length) {
50
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`, status: 400 };
51
+ }
52
+
53
+ if (!clusterTable?.name) {
54
+ return { message: 'not enough widget params: clusterTable name', status: 400 };
55
+ }
56
+
57
+ if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
58
+ return { message: 'invalid widget params: clusterTable pkey not found', status: 404 };
59
+ }
60
+
61
+ const { total = 0 } = pg.pk?.[table]
62
+ ? await pg.queryCache(`select oid::regclass as table, reltuples AS total from pg_class`)
63
+ .then((res) => res.rows?.find((row) => row.table === table))
64
+ : {};
65
+
66
+ const { bbox } = await pg.query(`select st_asgeojson(box2d(geom))::json as bbox from ${table} where ${where || '1=1'}`).then((res1) => res1.rows?.[0] || {});
67
+
68
+ // get sql
69
+ const { optimizedSQL } = filter || search
70
+ ? await funcs.getFilterSQL({ pg, table, filter, search })
71
+ : {};
72
+
73
+ const q = `select "${cluster}" as name, sum("${metrics[0]}") as metric
74
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
75
+ left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1
76
+ where ${where} group by ${cluster}, b.id`;
77
+
78
+ if (query.sql === '1') return q;
79
+
80
+ // auto Index
81
+ funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
82
+
83
+ const { rows = [] } = await pg.query(q);
84
+
85
+ return { rows, bbox, count: rows.length, total };
86
+ } catch (err) {
87
+ log.error('bi/cluster', { error: err.toString(), query });
88
+ return { error: err.toString(), status: 500 };
89
+ }
90
+ }
@@ -0,0 +1,144 @@
1
+ import Sphericalmercator from '@mapbox/sphericalmercator';
2
+
3
+ import path from 'path';
4
+ import { createHash } from 'crypto';
5
+ import { writeFile, mkdir } from 'fs/promises';
6
+ import { existsSync, readFileSync } from 'fs';
7
+
8
+ import yaml from '../../dashboard/controllers/utils/yaml.js';
9
+
10
+ const mercator = new Sphericalmercator({ size: 256 });
11
+
12
+ export default async function clusterVtile(req, reply) {
13
+ const {
14
+ pg, funcs, params = {}, query = {}, log,
15
+ } = req;
16
+ const { z, y } = params;
17
+ const x = params.x?.split('.')[0] - 0;
18
+
19
+ if (!x || !y || !z) {
20
+ return { message: 'not enough params: xyz', status: 400 };
21
+ }
22
+
23
+ const {
24
+ widget, filter, dashboard, search, clusterZoom, nocache, pointZoom,
25
+ } = query;
26
+
27
+ if (!widget) {
28
+ return { message: 'not enough params: widget', status: 400 };
29
+ }
30
+
31
+ const { config = {} } = funcs;
32
+
33
+ const cwd = process.cwd();
34
+ const widgetDir = dashboard ? path.join(cwd, 'server/templates/dashboard', dashboard) : path.join(cwd, 'server/templates/widget');
35
+
36
+ const filePath = dashboard ? path.join(widgetDir, (widget || 'index') + '.yml') : path.join(widgetDir, widget + '.yml');
37
+
38
+ if (!existsSync(filePath)) {
39
+ return { message: { root: config.local ? widgetDir : undefined, error: `not found`, widget }, status: 404 };
40
+ }
41
+
42
+ const headers = {
43
+ 'Content-Type': 'application/x-protobuf',
44
+ 'Cache-Control': nocache || query.sql || config.local
45
+ ? 'no-cache'
46
+ : 'public, max-age=86400',
47
+ };
48
+
49
+ const hash = [pointZoom, filter].filter((el) => el).join();
50
+
51
+ const root = funcs.getFolder(req);
52
+ const file = path.join(root, `/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`);
53
+
54
+ try {
55
+ const fileData = readFileSync(filePath, 'utf-8');
56
+ const { data = {} } = filePath.includes('.yml') ? yaml.loadSafe(fileData) : {};
57
+
58
+ if (!data?.table) {
59
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`, status: 400 };
60
+ }
61
+
62
+ const pkey = pg.pk?.[data?.table];
63
+
64
+ if (!pkey) {
65
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
66
+ }
67
+
68
+ // data param
69
+ const { table, query: where = '1=1', metrics = [], cluster, clusterTable = {} } = data;
70
+
71
+ if (!cluster) {
72
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`, status: 400 };
73
+ }
74
+
75
+ if (!metrics.length) {
76
+ return { message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`, status: 400 };
77
+ }
78
+
79
+ // get sql
80
+ const { optimizedSQL } = filter || search
81
+ ? await funcs.getFilterSQL({ pg, table, filter, search })
82
+ : {};
83
+
84
+ const q = `select "${cluster}" as name, sum("${metrics[0]}") as metric,
85
+ ${clusterTable?.name ? `b.*` : `q.${data?.geom || 'geom'} as geom`}
86
+ from ${optimizedSQL ? `(${optimizedSQL})` : table} q
87
+ ${clusterTable?.name
88
+ ? `left join lateral (select ${pg.pk?.[clusterTable?.name]
89
+ ? `"${pg.pk?.[clusterTable?.name]}" as id,`
90
+ : ''}
91
+ ${clusterTable?.geom || 'geom'} as geom from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1`
92
+ : ''}
93
+ where ${where} group by
94
+ ${cluster},
95
+ ${pg.pk?.[clusterTable?.name] ? `b.id,` : ''}
96
+ ${clusterTable?.name ? `b.${clusterTable?.geom || 'geom'}` : `q.${data?.geom || 'geom'}`}`;
97
+
98
+ if (query.sql === '1') return q;
99
+
100
+ const geomCol = parseInt(z, 10) < parseInt(pointZoom, 10) ? `ST_Centroid(${clusterTable?.geom || data?.geom || 'geom' })` : clusterTable?.geom || data?.geom || 'geom';
101
+
102
+ const bbox = mercator.bbox(+y, +x, +z, false/* , '900913' */);
103
+ const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
104
+
105
+ const q1 = `SELECT ST_AsMVT(q, '${table}', 4096, 'geom','row') as tile
106
+ FROM (
107
+ SELECT
108
+ floor(random() * 100000 + 1)::int + row_number() over() as row,
109
+
110
+ ${pg.pk?.[clusterTable?.name] ? 'id,' : ''} name, metric,
111
+
112
+ ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
113
+
114
+ FROM (select * from (${q})q where geom && ${bbox2d}
115
+
116
+ and geom is not null and st_srid(geom) >0
117
+
118
+ and ST_GeometryType(geom) = any ('{ "ST_Polygon", "ST_MultiPolygon" }')
119
+
120
+ limit 3000)q
121
+ ) q`;
122
+
123
+ if (query.sql === '2') return q1;
124
+
125
+ // auto Index
126
+ funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
127
+
128
+ const { rows = [] } = await pg.query(q1);
129
+
130
+ if (query.sql === '3') return rows.map((el) => el.tile);
131
+
132
+ const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
133
+
134
+ if (!nocache && !config.local) {
135
+ await mkdir(path.dirname(file), { recursive: true });
136
+ await writeFile(file, buffer, 'binary');
137
+ }
138
+
139
+ return reply.headers(headers).send(buffer);
140
+ } catch (err) {
141
+ log.error('bi/clusterVtile', { error: err.toString(), query, params });
142
+ return { error: err.toString(), status: 500 };
143
+ }
144
+ }
@@ -28,7 +28,7 @@ export default async function geojson(req, reply) {
28
28
  const cwd = process.cwd();
29
29
  const widgetDir = dashboard ? path.join(cwd, 'server/templates/dashboard', dashboard) : path.join(cwd, 'server/templates/widget');
30
30
 
31
- const filePath = dashboard ? path.join(widgetDir, widget || 'index' + '.yml') : path.join(widgetDir, widget + '.yml');
31
+ const filePath = dashboard ? path.join(widgetDir, (widget || 'index') + '.yml') : path.join(widgetDir, widget + '.yml');
32
32
 
33
33
  if (!existsSync(filePath)) {
34
34
  return { message: { root: config?.local ? widgetDir : undefined, error: `not found`, widget }, status: 404 };
@@ -0,0 +1,63 @@
1
+ import path from 'path';
2
+ import { existsSync, readFileSync } from 'fs';
3
+
4
+ import yaml from '../../dashboard/controllers/utils/yaml.js';
5
+
6
+ export default async function map({
7
+ pg, funcs = {}, query = {},
8
+ }) {
9
+ const { config = {} } = funcs;
10
+ const { widget, dashboard } = query;
11
+
12
+ const cwd = process.cwd();
13
+ const widgetDir = dashboard ? path.join(cwd, 'server/templates/dashboard', dashboard) : path.join(cwd, 'server/templates/widget');
14
+
15
+ const filePath = dashboard ? path.join(widgetDir, (widget || 'index') + '.yml') : path.join(widgetDir, widget + '.yml');
16
+
17
+ if (!existsSync(filePath)) {
18
+ return { message: { root: config?.local ? widgetDir : undefined, error: `not found`, widget }, status: 404 };
19
+ }
20
+
21
+ try {
22
+ const fileData = readFileSync(filePath, 'utf-8');
23
+ const { data = {}, type } = filePath.includes('.yml') ? yaml.loadSafe(fileData) : {};
24
+ if (!['map' /*, 'mapCluster' */].includes(type)) {
25
+ return { message: 'access restricted: invalid widget type', status: 403 };
26
+ }
27
+ if (!data?.table) {
28
+ return { message: 'invalid widget: param table is required', status: 400 };
29
+ }
30
+ if (!pg.pk[data?.table]) {
31
+ return { message: 'invalid widget: table pkey not found', status: 400 };
32
+ }
33
+
34
+ const { q = '' } = await funcs.getFilterSQL({
35
+ pg, table: data?.table, filter: query.filter,
36
+ });
37
+
38
+ const res = {};
39
+ if (data?.color) {
40
+ const { rows = [] } = await pg.query(`select count(*), "${data.color}" as val from ${data.table} where ${data.query || '1=1'} group by "${data.color}"`);
41
+ Object.assign(res, { colors: rows }); // кольори для легенди
42
+ }
43
+ if (data?.metrics?.length) {
44
+ const metric = data?.metrics[0];
45
+ const q1 = `select PERCENTILE_CONT(0) WITHIN GROUP (ORDER BY "${metric}") as "0",
46
+ PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "${metric}") as "25",
47
+ PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY "${metric}") as "50",
48
+ PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "${metric}") as "75",
49
+ PERCENTILE_CONT(1) WITHIN GROUP (ORDER BY "${metric}") as "100" from ${data.table} where ${data.query || '1=1'} and ${q || '1=1'}`;
50
+ const sizes = await pg.query(q1).then((res1) => Object.values(res1.rows?.[0] || {}));
51
+ Object.assign(res, { sizes }); // розміри для легенди
52
+ }
53
+ const { bbox } = await pg.query(`select st_asgeojson(box2d(geom))::json as bbox from ${data.table} where ${data.query || '1=1'}`).then((res1) => res1.rows?.[0] || {});
54
+ Object.assign(res, {
55
+ bbox, // Map bounds
56
+ top: [], // 10 найкращих
57
+ bottom: [], // 10 найгірших
58
+ });
59
+ return res;
60
+ } catch (err) {
61
+ return { error: err.toString(), status: 500 };
62
+ }
63
+ }
@@ -91,7 +91,7 @@ export default async function vtile(req, reply) {
91
91
  }
92
92
 
93
93
  // get sql
94
- const filterQ = filter ? await funcs.getFilterSQL({ pg, table, filter, query }) : undefined;
94
+ const filterQ = filter ? await funcs.getFilterSQL({ pg, table, filter }) : undefined;
95
95
  const q = `select "${pkey}", "${xName?.name}", /* st_asgeojson(geom)::json as */ ${geom} as geom from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
96
96
 
97
97
  if (sql === '1') return q;
@@ -1,8 +1,10 @@
1
- // import config from '../../../config.js';
2
-
1
+ import map from './controllers/map.js';
3
2
  import geojson from './controllers/geojson.js';
4
3
  import vtile from './controllers/vtile.js';
5
4
 
5
+ import cluster from './controllers/cluster.js';
6
+ import clusterVtile from './controllers/clusterVtile.js';
7
+
6
8
  const biSchema = {
7
9
  querystring: {
8
10
  widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
@@ -14,9 +16,10 @@ const biSchema = {
14
16
  },
15
17
  };
16
18
 
17
-
18
19
  export default async function route(fastify, opts) {
19
- // const prefix = opts?.prefix || config.prefix || '/api';
20
+ fastify.get('/bi-map', { schema: biSchema }, map);
20
21
  fastify.get('/bi-geojson', { schema: biSchema }, geojson);
21
22
  fastify.get('/bi-vtile/:z/:y/:x', { schema: biSchema }, vtile);
23
+ fastify.get('/bi-cluster', { schema: biSchema }, cluster);
24
+ fastify.get('/bi-cluster-vtile/:z/:y/:x', { schema: biSchema }, clusterVtile);
22
25
  }
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "text": "Теплиця",
5
+ "color": "#4682B4"
6
+ },
7
+ {
8
+ "id": "2",
9
+ "text": "Сад",
10
+ "color": "#228B22"
11
+ }
12
+ ]
@@ -0,0 +1,18 @@
1
+ type: funnel
2
+ title: Funnel
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+
13
+ x: order_date
14
+ granularity: quarter
15
+ groupby: product_line
16
+
17
+ controls:
18
+ style:
@@ -0,0 +1,18 @@
1
+ type: heatmap
2
+ title: Heatmap
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+
13
+ x: order_date
14
+ granularity: quarter
15
+ groupby: product_line
16
+
17
+ controls:
18
+ style:
@@ -0,0 +1,58 @@
1
+ title: Sales Dashboard
2
+ description: This example dashboard provides insight into the business operations of vehicle seller
3
+ table_name: demo.cleaned_sales_data
4
+ panels:
5
+ # row 1
6
+ - type: column
7
+ col: 3
8
+ widgets:
9
+ - total_revenue
10
+ - total_products_sold
11
+
12
+ - widget: quarterly_revenue
13
+ col: 6
14
+ - widget: vehicle_sales_info
15
+ col: 3
16
+
17
+ # row 2
18
+ - widget: total_products_sold_by_product_line
19
+ col: 3
20
+ - widget: quarterly_revenue_by_product_line
21
+ col: 6
22
+ - widget: total_revenue_by_product_line
23
+ col: 3
24
+ #row 3
25
+ - widget: line
26
+ col: 3
27
+ - widget: pivot
28
+ col: 6
29
+ - widget: stat
30
+ col: 3
31
+ #row 4
32
+ - widget: progress
33
+ col: 3
34
+ - widget: funnel
35
+ col: 3
36
+ - widget: heatmap
37
+ col: 6
38
+ #row 5
39
+ - widget: waterfall
40
+ col: 3
41
+ - widget: map
42
+ col: 8
43
+ grid: # сітка якщо є
44
+ - { 'x': 0, 'y': 0, 'w': 2, 'h': 2, 'i': 0, widget: 'quarterly_revenue' }
45
+ - { 'x': 1, 'y': 0, 'w': 2, 'h': 2, 'i': 0, widget: 'total_revenue' }
46
+
47
+ widgets: null # самі віджети
48
+
49
+ filters: null # фільтри
50
+
51
+ style:
52
+ color:
53
+ stack: true # only for bar
54
+ orientation: vertical # only for bar, line
55
+ date_format: smart_date # d3 format number
56
+ number_format: smart_date # d3 format number
57
+ label: 222
58
+ tooltip: 333
@@ -0,0 +1,19 @@
1
+ type: line
2
+ title: Line bar
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+
13
+ x: order_date
14
+ granularity: quarter
15
+ orderby: year
16
+
17
+ controls:
18
+ style:
19
+ area: true
@@ -0,0 +1,13 @@
1
+ type: map
2
+ title: Регіональний розподіл
3
+
4
+ data:
5
+ table: demo.parcel_object_culture # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Розмір точки
9
+ - obj_area
10
+ color: object_type # color
11
+ clusterZoom: 15
12
+ columns: # додаткові колонки
13
+ - obj_area
@@ -0,0 +1,18 @@
1
+ type: pivot
2
+ title: Pivot
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+
13
+ x: order_date
14
+ granularity: quarter
15
+ groupby: product_line
16
+
17
+ controls:
18
+ style:
@@ -0,0 +1,15 @@
1
+ type: progress
2
+ title: Progress bar
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+ x: product_line
13
+
14
+ controls:
15
+ style:
@@ -0,0 +1,17 @@
1
+ type: bar
2
+ title: Quarterly Revenue
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+
13
+ x: order_date
14
+ granularity: quarter
15
+ orderby: year
16
+
17
+ controls:
@@ -0,0 +1,19 @@
1
+ type: bar
2
+ title: Quarterly Revenue (By Product Line)
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+
13
+ x: order_date
14
+ granularity: quarter
15
+ groupby: product_line
16
+
17
+ controls:
18
+ style:
19
+ stack: true
@@ -0,0 +1,15 @@
1
+ type: stat
2
+ title: Stat bar
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: sales
10
+ operator: sum
11
+ title: sales
12
+ x: product_line
13
+
14
+ controls:
15
+ style:
@@ -0,0 +1,9 @@
1
+ type: number
2
+ title: Total Products Sold
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+ metrics:
8
+ - quantity_ordered
9
+ controls:
@@ -0,0 +1,12 @@
1
+ type: listbar
2
+ title: Total Products Sold (By Product Line)
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+
8
+ metrics: # Групування
9
+ - name: quantity_ordered
10
+ operator: sum
11
+ title: quantity_sold
12
+ x: product_line
@@ -0,0 +1,10 @@
1
+ type: number
2
+ title: Total Revenue
3
+
4
+ data:
5
+ table: demo.cleaned_sales_data # Назва таблиці
6
+ query: 1=1 # Запит
7
+ metrics: sales
8
+ controls:
9
+ style:
10
+ prefix: '$'