@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.
- package/dist/assets/charts/bar.png +0 -0
- package/dist/assets/charts/funnel.png +0 -0
- package/dist/assets/charts/no_data.jpg +0 -0
- package/dist/assets/charts/number.png +0 -0
- package/dist/assets/charts/pie.png +0 -0
- package/dist/assets/charts/progress.png +0 -0
- package/dist/assets/charts/stat.png +0 -0
- package/dist/assets/images/bar.png +0 -0
- package/dist/assets/images/funnel.png +0 -0
- package/dist/assets/images/no_data.jpg +0 -0
- package/dist/assets/images/number.png +0 -0
- package/dist/assets/images/pie.png +0 -0
- package/dist/assets/images/progress.png +0 -0
- package/dist/assets/images/stat.png +0 -0
- package/dist/bi.js +1 -1
- package/dist/bi.umd.cjs +723 -113
- package/dist/{import-file-D6RYWvi_.js → import-file-Db7C78fp.js} +43157 -43204
- package/dist/style.css +1 -1
- package/dist/vs-calendar-Ddl6WRL3.js +96 -0
- package/dist/vs-funnel-bar-GbisTylf.js +92 -0
- package/dist/vs-heatmap-CPiim0yg.js +83 -0
- package/dist/vs-map-DuBKvlTI.js +19422 -0
- package/dist/vs-number-CR1H0JTM.js +39 -0
- package/dist/{vs-text-UyIWGqQO.js → vs-text-C3RkizPQ.js} +60 -60
- package/package.json +7 -5
- package/server/routes/dashboard/controllers/dashboard.js +20 -1
- package/server/routes/data/controllers/data.js +31 -19
- package/server/routes/data/controllers/util/chartSQL.js +8 -7
- package/server/routes/data/controllers/util/normalizeData.js +10 -4
- package/server/routes/edit/controllers/dashboard.add.js +5 -1
- package/server/routes/edit/controllers/dashboard.edit.js +5 -1
- package/server/routes/edit/controllers/widget.add.js +23 -7
- package/server/routes/edit/controllers/widget.del.js +1 -1
- package/server/routes/edit/controllers/widget.edit.js +8 -5
- package/server/routes/map/controllers/cluster.js +90 -0
- package/server/routes/map/controllers/clusterVtile.js +144 -0
- package/server/routes/map/controllers/geojson.js +1 -1
- package/server/routes/map/controllers/map.js +63 -0
- package/server/routes/map/controllers/vtile.js +1 -1
- package/server/routes/map/index.mjs +7 -4
- package/server/templates/cls/demo.parcel.object_type.json +12 -0
- package/server/templates/dashboard/demo/funnel.yml +18 -0
- package/server/templates/dashboard/demo/heatmap.yml +18 -0
- package/server/templates/dashboard/demo/index.yml +58 -0
- package/server/templates/dashboard/demo/line.yml +19 -0
- package/server/templates/dashboard/demo/map.yml +13 -0
- package/server/templates/dashboard/demo/pivot.yml +18 -0
- package/server/templates/dashboard/demo/progress.yml +15 -0
- package/server/templates/dashboard/demo/quarterly_revenue.yml +17 -0
- package/server/templates/dashboard/demo/quarterly_revenue_by_product_line.yml +19 -0
- package/server/templates/dashboard/demo/stat.yml +15 -0
- package/server/templates/dashboard/demo/total_products_sold.yml +9 -0
- package/server/templates/dashboard/demo/total_products_sold_by_product_line.yml +12 -0
- package/server/templates/dashboard/demo/total_revenue.yml +10 -0
- package/server/templates/dashboard/demo/total_revenue_by_product_line.yml +20 -0
- package/server/templates/dashboard/demo/vehicle_sales_info.md +17 -0
- package/server/templates/dashboard/demo/waterfall.yml +19 -0
- package/server/templates/dashboard/erobota/bar_area.yml +3 -1
- package/server/templates/dashboard/erobota/bar_culture.yml +1 -1
- package/server/templates/dashboard/erobota/bar_grand.yml +3 -2
- package/server/templates/dashboard/erobota/count_grand.yml +1 -1
- package/server/templates/dashboard/erobota/index.yml +14 -12
- package/server/templates/dashboard/erobota/list_culture.yml +1 -1
- package/server/templates/dashboard/erobota/list_grant.yml +1 -1
- package/server/templates/dashboard/erobota/map.yml +1 -1
- package/server/templates/dashboard/erobota/pie_area.yml +1 -1
- package/server/templates/dashboard/erobota/pie_grant.yml +1 -1
- package/server/templates/dashboard/erobota/total_area.yml +1 -1
- package/server/templates/dashboard/erobota/total_grand.yml +1 -1
- package/server/templates/dashboard/map/index.yml +6 -0
- package/server/templates/dashboard/map/map.yml +13 -0
- package/server/templates/dashboard/map/mapCluster.yml +16 -0
- package/server/templates/dashboard/sales/index.yml +4 -3
- package/server/templates/dashboard/sales/quarterly_revenue.yml +1 -3
- package/server/templates/dashboard/sales/quarterly_revenue_by_product_line.yml +1 -1
- package/server/templates/dashboard/sales/total_products_sold.yml +1 -1
- package/server/templates/dashboard/sales/total_products_sold_by_product_line.yml +2 -1
- package/server/templates/dashboard/sales/total_revenue.yml +1 -1
- package/server/templates/dashboard/sales/total_revenue_by_product_line.yml +4 -1
- package/server/templates/dashboard/test3/quarterly_revenue.yml +1 -1
- package/server/templates/dashboard/test3/widget1.yml +1 -1
- package/server/templates/widget/calendar.yml +14 -0
- package/server/templates/widget/map.yml +15 -0
- package/server/templates/widget/mapCluster.yml +16 -0
- package/server/templates/widget/negative.yml +18 -0
- package/server/templates/widget/negative_profi_expense.yml +23 -0
- package/server/templates/widget/negative_type.yml +24 -0
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,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,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
|