Merge branch 'main' into merge/mainto3.0
This commit is contained in:
commit
62da23b848
|
@ -19,7 +19,7 @@ Perform the following installation operations in the Linux system:
|
||||||
- Python version 3.10 or higher has been installed (if not installed, please refer to [Python Installation](https://docs.python.org/)).
|
- Python version 3.10 or higher has been installed (if not installed, please refer to [Python Installation](https://docs.python.org/)).
|
||||||
- Download or clone the [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) project. After entering the root directory of the project, run the "install.sh" script to download and install the TDengine client library and related dependencies locally.
|
- Download or clone the [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) project. After entering the root directory of the project, run the "install.sh" script to download and install the TDengine client library and related dependencies locally.
|
||||||
|
|
||||||
## Data Analysis
|
## Visualize data
|
||||||
|
|
||||||
**Step 1**, Run the "run.sh" script in the root directory of the [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) project to start the Perspective service. This service will retrieve data from the TDengine database every 300 milliseconds and transmit the data in a streaming form to the web-based `Perspective Viewer`.
|
**Step 1**, Run the "run.sh" script in the root directory of the [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) project to start the Perspective service. This service will retrieve data from the TDengine database every 300 milliseconds and transmit the data in a streaming form to the web-based `Perspective Viewer`.
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ sh run.sh
|
||||||
python -m http.server 8081
|
python -m http.server 8081
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The effect presented after accessing the web page through the browser is shown in the following figure:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Instructions for use
|
## Instructions for use
|
||||||
|
@ -56,40 +58,9 @@ The `perspective_server.py` script in the root directory of the [perspective-con
|
||||||
3. Create a Perspective table (the table structure needs to match the type of the table in the TDengine database).
|
3. Create a Perspective table (the table structure needs to match the type of the table in the TDengine database).
|
||||||
4. Call the `Tornado.PeriodicCallback` function to start a scheduled task, thereby achieving the update of the data in the Perspective table. The sample code is as follows:
|
4. Call the `Tornado.PeriodicCallback` function to start a scheduled task, thereby achieving the update of the data in the Perspective table. The sample code is as follows:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def perspective_thread(perspective_server: perspective.Server, tdengine_conn: taosws.Connection):
|
{{#include docs/examples/perspective/perspective_server.py:perspective_server}}
|
||||||
"""
|
```
|
||||||
Create a new Perspective table and update it with new data every 50ms
|
|
||||||
"""
|
|
||||||
# create a new Perspective table
|
|
||||||
client = perspective_server.new_local_client()
|
|
||||||
schema = {
|
|
||||||
"timestamp": datetime,
|
|
||||||
"location": str,
|
|
||||||
"groupid": int,
|
|
||||||
"current": float,
|
|
||||||
"voltage": int,
|
|
||||||
"phase": float,
|
|
||||||
}
|
|
||||||
# define the table schema
|
|
||||||
table = client.table(
|
|
||||||
schema,
|
|
||||||
limit=1000, # maximum number of rows in the table
|
|
||||||
name=PERSPECTIVE_TABLE_NAME, # table name. Use this with perspective-viewer on the client side
|
|
||||||
)
|
|
||||||
logger.info("Created new Perspective table")
|
|
||||||
|
|
||||||
# update with new data
|
|
||||||
def updater():
|
|
||||||
data = read_tdengine(tdengine_conn)
|
|
||||||
table.update(data)
|
|
||||||
logger.debug(f"Updated Perspective table: {len(data)} rows")
|
|
||||||
|
|
||||||
logger.info(f"Starting tornado ioloop update loop every {PERSPECTIVE_REFRESH_RATE} milliseconds")
|
|
||||||
# start the periodic callback to update the table data
|
|
||||||
callback = tornado.ioloop.PeriodicCallback(callback=updater, callback_time=PERSPECTIVE_REFRESH_RATE)
|
|
||||||
callback.start()
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTML Page Configuration
|
### HTML Page Configuration
|
||||||
|
|
||||||
|
@ -100,77 +71,7 @@ The `prsp-viewer.html` file in the root directory of the [perspective-connect-de
|
||||||
- Import the Perspective library, connect to the Perspective server via a WebSocket, and load the `meters_values` table to display dynamic data.
|
- Import the Perspective library, connect to the Perspective server via a WebSocket, and load the `meters_values` table to display dynamic data.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
{{#include docs/examples/perspective/prsp-viewer.html:perspective_viewer}}
|
||||||
// import the Perspective library
|
|
||||||
import perspective from "https://unpkg.com/@finos/perspective@3.1.3/dist/cdn/perspective.js";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async function () {
|
|
||||||
// an asynchronous function for loading the view
|
|
||||||
async function load_viewer(viewerId, config) {
|
|
||||||
try {
|
|
||||||
const table_name = "meters_values";
|
|
||||||
const viewer = document.getElementById(viewerId);
|
|
||||||
// connect Perspective WebSocket server
|
|
||||||
const websocket = await perspective.websocket("ws://localhost:8085/websocket");
|
|
||||||
// open server table
|
|
||||||
const server_table = await websocket.open_table(table_name);
|
|
||||||
// load the table into the view
|
|
||||||
await viewer.load(server_table);
|
|
||||||
// use view configuration
|
|
||||||
await viewer.restore(config);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to get data from ${table_name}, err: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// configuration of the view
|
|
||||||
const config = {
|
|
||||||
"version": "3.3.1", // Perspective library version (compatibility identifier)
|
|
||||||
"plugin": "Datagrid", // View mode: Datagrid (table) or D3FC (chart)
|
|
||||||
"plugin_config": { // Plugin-specific configuration
|
|
||||||
"columns": {
|
|
||||||
"current": {
|
|
||||||
"width": 150 // Column width in pixels
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"edit_mode": "READ_ONLY", // Edit mode: READ_ONLY (immutable) or EDIT (editable)
|
|
||||||
"scroll_lock": false // Whether to lock scroll position
|
|
||||||
},
|
|
||||||
"columns_config": {}, // Custom column configurations (colors, formatting, etc.)
|
|
||||||
"settings": true, // Whether to show settings panel (true/false)
|
|
||||||
"theme": "Power Meters", // Custom theme name (must be pre-defined)
|
|
||||||
"title": "Meters list data", // View title
|
|
||||||
"group_by": ["location", "groupid"], // Row grouping fields (equivalent to `row_pivots`)
|
|
||||||
"split_by": [], // Column grouping fields (equivalent to `column_pivots`)
|
|
||||||
"columns": [ // Columns to display (in order)
|
|
||||||
"timestamp",
|
|
||||||
"location",
|
|
||||||
"current",
|
|
||||||
"voltage",
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"filter": [], // Filter conditions (triplet format array)
|
|
||||||
"sort": [], // Sorting rules (format: [field, direction])
|
|
||||||
"expressions": {}, // Custom expressions (e.g., calculated columns)
|
|
||||||
"aggregates": { // Aggregation function configuration
|
|
||||||
"timestamp": "last", // Aggregation: last (takes the latest value)
|
|
||||||
"voltage": "last", // Aggregation: last
|
|
||||||
"phase": "last", // Aggregation: last
|
|
||||||
"current": "last" // Aggregation: last
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// load the first view
|
|
||||||
await load_viewer("prsp-viewer-1", config1);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Define the HTML Structure of the Dashboard -->
|
|
||||||
<div id="dashboard">
|
|
||||||
<div class="viewer-container">
|
|
||||||
<perspective-viewer id="prsp-viewer-1" theme="Pro Dark"></perspective-viewer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reference Materials
|
## Reference Materials
|
||||||
|
|
|
@ -0,0 +1,207 @@
|
||||||
|
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||||
|
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||||
|
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||||
|
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||||
|
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||||
|
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||||
|
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||||
|
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||||
|
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||||
|
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tornado.websocket
|
||||||
|
import tornado.web
|
||||||
|
import tornado.ioloop
|
||||||
|
from datetime import date, datetime
|
||||||
|
import perspective
|
||||||
|
import perspective.handlers.tornado
|
||||||
|
import json
|
||||||
|
import taosws
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger('main')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TDengine connection parameters
|
||||||
|
# =============================================================================
|
||||||
|
TAOS_HOST = "localhost" # TDengine server host
|
||||||
|
TAOS_PORT = 6041 # TDengine server port
|
||||||
|
TAOS_USER = "root" # TDengine username
|
||||||
|
TAOS_PASSWORD = "taosdata" # TDengine password
|
||||||
|
|
||||||
|
TAOS_DATABASE = "power" # TDengine database name
|
||||||
|
TAOS_TABLENAME = "meters" # TDengine table name
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Perspective server parameters
|
||||||
|
# =============================================================================
|
||||||
|
PERSPECTIVE_TABLE_NAME = "meters_values" # name of the Perspective table
|
||||||
|
PERSPECTIVE_REFRESH_RATE = 250 # refresh rate in milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
class CustomJSONEncoder(json.JSONEncoder):
|
||||||
|
"""
|
||||||
|
Custom JSON encoder that serializes datetime and date objects
|
||||||
|
"""
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
elif isinstance(obj, date):
|
||||||
|
return obj.isoformat()
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
json.JSONEncoder.default = CustomJSONEncoder().default
|
||||||
|
|
||||||
|
|
||||||
|
def convert_ts(ts) -> datetime:
|
||||||
|
"""
|
||||||
|
Convert a timestamp string to a datetime object
|
||||||
|
"""
|
||||||
|
for fmt in ('%Y-%m-%d %H:%M:%S.%f %z', '%Y-%m-%d %H:%M:%S %z'):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(ts, fmt)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
raise ValueError(f"Time data '{ts}' does not match any format")
|
||||||
|
|
||||||
|
|
||||||
|
def create_tdengine_connection(
|
||||||
|
host: str = TAOS_HOST,
|
||||||
|
port: int = TAOS_PORT,
|
||||||
|
user: str = TAOS_USER,
|
||||||
|
password: str = TAOS_PASSWORD,
|
||||||
|
) -> taosws.Connection:
|
||||||
|
try:
|
||||||
|
# connect to the tdengine server
|
||||||
|
conn = taosws.connect(
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
)
|
||||||
|
# switch to the right database
|
||||||
|
conn.execute(f"USE {TAOS_DATABASE}")
|
||||||
|
# connection successful
|
||||||
|
logger.info(f"Connected to tdengine successfully: {host}:{port}")
|
||||||
|
return conn
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"Failed to connect to tdengine: {host}:{port} -- ErrMessage: {err}")
|
||||||
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
def read_tdengine(
|
||||||
|
conn: taosws.Connection,
|
||||||
|
) -> list[dict]:
|
||||||
|
try:
|
||||||
|
# query the database
|
||||||
|
sql = f"""
|
||||||
|
SELECT `ts`, location, groupid, current, voltage, phase
|
||||||
|
FROM {TAOS_TABLENAME}
|
||||||
|
WHERE `ts` >= NOW() - 12h
|
||||||
|
ORDER BY `ts` DESC
|
||||||
|
LIMIT 1000
|
||||||
|
"""
|
||||||
|
logger.debug(f"Executing query: {sql}")
|
||||||
|
res = conn.query(sql)
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"timestamp": convert_ts(row[0]),
|
||||||
|
"location": row[1],
|
||||||
|
"groupid": row[2],
|
||||||
|
"current": row[3],
|
||||||
|
"voltage": row[4],
|
||||||
|
"phase": row[5],
|
||||||
|
}
|
||||||
|
for row in res
|
||||||
|
]
|
||||||
|
logger.info(f"select result: {data}")
|
||||||
|
return data
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"Failed to query tdengine: {err}")
|
||||||
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
// ANCHOR: perspective_server
|
||||||
|
def perspective_thread(perspective_server: perspective.Server, tdengine_conn: taosws.Connection):
|
||||||
|
"""
|
||||||
|
Create a new Perspective table and update it with new data every 50ms
|
||||||
|
"""
|
||||||
|
# create a new Perspective table
|
||||||
|
client = perspective_server.new_local_client()
|
||||||
|
schema = {
|
||||||
|
"timestamp": datetime,
|
||||||
|
"location": str,
|
||||||
|
"groupid": int,
|
||||||
|
"current": float,
|
||||||
|
"voltage": int,
|
||||||
|
"phase": float,
|
||||||
|
}
|
||||||
|
# define the table schema
|
||||||
|
table = client.table(
|
||||||
|
schema,
|
||||||
|
limit=1000, # maximum number of rows in the table
|
||||||
|
name=PERSPECTIVE_TABLE_NAME, # table name. Use this with perspective-viewer on the client side
|
||||||
|
)
|
||||||
|
logger.info("Created new Perspective table")
|
||||||
|
|
||||||
|
# update with new data
|
||||||
|
def updater():
|
||||||
|
data = read_tdengine(tdengine_conn)
|
||||||
|
table.update(data)
|
||||||
|
logger.debug(f"Updated Perspective table: {len(data)} rows")
|
||||||
|
|
||||||
|
logger.info(f"Starting tornado ioloop update loop every {PERSPECTIVE_REFRESH_RATE} milliseconds")
|
||||||
|
# start the periodic callback to update the table data
|
||||||
|
callback = tornado.ioloop.PeriodicCallback(callback=updater, callback_time=PERSPECTIVE_REFRESH_RATE)
|
||||||
|
callback.start()
|
||||||
|
|
||||||
|
// ANCHOR_END: perspective_server
|
||||||
|
|
||||||
|
def make_app(perspective_server):
|
||||||
|
"""
|
||||||
|
Create a new Tornado application with a websocket handler that
|
||||||
|
serves a Perspective table. PerspectiveTornadoHandler handles
|
||||||
|
the websocket connection and streams the Perspective table changes
|
||||||
|
to the client.
|
||||||
|
"""
|
||||||
|
return tornado.web.Application([
|
||||||
|
(
|
||||||
|
r"/websocket", # websocket endpoint. Use this URL to configure the websocket client OR Prospective Server adapter
|
||||||
|
perspective.handlers.tornado.PerspectiveTornadoHandler, # PerspectiveTornadoHandler handles perspective table updates <-> websocket client
|
||||||
|
{"perspective_server": perspective_server}, # pass the perspective server to the handler
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logger.info("TDEngine <-> Perspective Demo")
|
||||||
|
|
||||||
|
# create a new Perspective server
|
||||||
|
logger.info("Creating new Perspective server")
|
||||||
|
perspective_server = perspective.Server()
|
||||||
|
# create the tdengine connection
|
||||||
|
logger.info("Creating new TDEngine connection")
|
||||||
|
tdengine_conn = create_tdengine_connection()
|
||||||
|
|
||||||
|
# setup and start the Tornado app
|
||||||
|
logger.info("Creating Tornado server")
|
||||||
|
app = make_app(perspective_server)
|
||||||
|
app.listen(8085, address='0.0.0.0')
|
||||||
|
logger.info("Listening on http://localhost:8080")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# start the io loop
|
||||||
|
logger.info("Starting ioloop to update Perspective table data via tornado websocket...")
|
||||||
|
loop = tornado.ioloop.IOLoop.current()
|
||||||
|
loop.call_later(0, perspective_thread, perspective_server, tdengine_conn)
|
||||||
|
loop.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.warning("Keyboard interrupt detected. Shutting down tornado server...")
|
||||||
|
loop.stop()
|
||||||
|
loop.close()
|
||||||
|
logging.info("Shut down")
|
|
@ -0,0 +1,135 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Perspective Viewer Dashboard</title>
|
||||||
|
<link rel="stylesheet" crossorigin="anonymous"
|
||||||
|
href="https://unpkg.com/@finos/perspective-viewer/dist/css/themes.css"/>
|
||||||
|
<style>
|
||||||
|
/* define the layout of the entire dashboard */
|
||||||
|
#dashboard {
|
||||||
|
display: grid;
|
||||||
|
/* define a grid layout with two rows and two columns */
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
/* limit the maximum height of the Dashboard to the viewport height */
|
||||||
|
max-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* define the style */
|
||||||
|
.viewer-container {
|
||||||
|
/* adjust the height of the container to ensure it can be displayed on one screen */
|
||||||
|
height: calc((100vh - 30px) / 2);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
perspective-viewer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #242526;
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- introduce JavaScript files related to Perspective Viewer -->
|
||||||
|
<script type="module" src="https://unpkg.com/@finos/perspective@3.1.3/dist/cdn/perspective.js"></script>
|
||||||
|
<script type="module" src="https://unpkg.com/@finos/perspective-viewer@3.1.3/dist/cdn/perspective-viewer.js"></script>
|
||||||
|
<script type="module"
|
||||||
|
src="https://unpkg.com/@finos/perspective-viewer-datagrid@3.1.3/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||||
|
<script type="module"
|
||||||
|
src="https://unpkg.com/@finos/perspective-viewer-d3fc@3.1.3/dist/cdn/perspective-viewer-d3fc.js"></script>
|
||||||
|
|
||||||
|
// ANCHOR: perspective_viewer
|
||||||
|
<script type="module">
|
||||||
|
// import the Perspective library
|
||||||
|
import perspective from "https://unpkg.com/@finos/perspective@3.1.3/dist/cdn/perspective.js";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async function () {
|
||||||
|
// an asynchronous function for loading the view
|
||||||
|
async function load_viewer(viewerId, config) {
|
||||||
|
try {
|
||||||
|
const table_name = "meters_values";
|
||||||
|
const viewer = document.getElementById(viewerId);
|
||||||
|
// connect WebSocket server
|
||||||
|
const websocket = await perspective.websocket("ws://localhost:8085/websocket");
|
||||||
|
// open server table
|
||||||
|
const server_table = await websocket.open_table(table_name);
|
||||||
|
// load the table into the view
|
||||||
|
await viewer.load(server_table);
|
||||||
|
// use view configuration
|
||||||
|
await viewer.restore(config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发生错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// configuration of the view
|
||||||
|
const config1 = {
|
||||||
|
"version": "3.3.1", // Perspective library version (compatibility identifier)
|
||||||
|
"plugin": "Datagrid", // View mode: Datagrid (table) or D3FC (chart)
|
||||||
|
"plugin_config": { // Plugin-specific configuration
|
||||||
|
"columns": {
|
||||||
|
"current": {
|
||||||
|
"width": 150 // Column width in pixels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit_mode": "READ_ONLY", // Edit mode: READ_ONLY (immutable) or EDIT (editable)
|
||||||
|
"scroll_lock": false // Whether to lock scroll position
|
||||||
|
},
|
||||||
|
"columns_config": {}, // Custom column configurations (colors, formatting, etc.)
|
||||||
|
"settings": true, // Whether to show settings panel (true/false)
|
||||||
|
"theme": "Power Meters", // Custom theme name (must be pre-defined)
|
||||||
|
"title": "Meters list data", // View title
|
||||||
|
"group_by": ["location", "groupid"], // Row grouping fields (equivalent to `row_pivots`)
|
||||||
|
"split_by": [], // Column grouping fields (equivalent to `column_pivots`)
|
||||||
|
"columns": [ // Columns to display (in order)
|
||||||
|
"timestamp",
|
||||||
|
"location",
|
||||||
|
"current",
|
||||||
|
"voltage",
|
||||||
|
"phase"
|
||||||
|
],
|
||||||
|
"filter": [], // Filter conditions (triplet format array)
|
||||||
|
"sort": [], // Sorting rules (format: [field, direction])
|
||||||
|
"expressions": {}, // Custom expressions (e.g., calculated columns)
|
||||||
|
"aggregates": { // Aggregation function configuration
|
||||||
|
"timestamp": "last", // Aggregation: last (takes the latest value)
|
||||||
|
"voltage": "last", // Aggregation: last
|
||||||
|
"phase": "last", // Aggregation: last
|
||||||
|
"current": "last" // Aggregation: last
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// load the first view
|
||||||
|
await load_viewer("prsp-viewer-1", config1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- define the HTML Structure of the Dashboard -->
|
||||||
|
<div id="dashboard">
|
||||||
|
<div class="viewer-container">
|
||||||
|
<perspective-viewer id="prsp-viewer-1" theme="Pro Dark"></perspective-viewer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
// ANCHOR_END: perspective_viewer
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -22,7 +22,7 @@ Perspective 是一款开源且强大的数据可视化库,由 [Prospective.co]
|
||||||
- Python 3.10 及以上版本已安装(如未安装,可参考 [Python 安装](https://docs.python.org/)。
|
- Python 3.10 及以上版本已安装(如未安装,可参考 [Python 安装](https://docs.python.org/)。
|
||||||
- 下载或克隆 [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) 项目,进入项目根目录后运行 “install.sh” 脚本,以便在本地下载并安装 TDengine 客户端库以及相关的依赖项。
|
- 下载或克隆 [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) 项目,进入项目根目录后运行 “install.sh” 脚本,以便在本地下载并安装 TDengine 客户端库以及相关的依赖项。
|
||||||
|
|
||||||
## 数据分析
|
## 可视化数据
|
||||||
|
|
||||||
**第 1 步**,运行 [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) 项目根目录中的 “run.sh” 脚本,以此启动 Perspective 服务。该服务会每隔 300 毫秒从 TDengine 数据库中获取一次数据,并将数据以流的形式传输至基于 Web 的 `Perspective Viewer` 。
|
**第 1 步**,运行 [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) 项目根目录中的 “run.sh” 脚本,以此启动 Perspective 服务。该服务会每隔 300 毫秒从 TDengine 数据库中获取一次数据,并将数据以流的形式传输至基于 Web 的 `Perspective Viewer` 。
|
||||||
|
|
||||||
|
@ -36,6 +36,8 @@ sh run.sh
|
||||||
python -m http.server 8081
|
python -m http.server 8081
|
||||||
```
|
```
|
||||||
|
|
||||||
|
通过浏览器访问该 Web 页面后所呈现出的效果如下图所示:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
|
@ -59,40 +61,9 @@ Python 连接器详细写入说明可参见 [Python 参数绑定](../../../refer
|
||||||
1. 创建一个 Perspective 表(表结构需要与 TDengine 数据库中表的类型保持匹配)。
|
1. 创建一个 Perspective 表(表结构需要与 TDengine 数据库中表的类型保持匹配)。
|
||||||
1. 调用 `Tornado.PeriodicCallback` 函数来启动定时任务,进而实现对 Perspective 表数据的更新,示例代码如下:
|
1. 调用 `Tornado.PeriodicCallback` 函数来启动定时任务,进而实现对 Perspective 表数据的更新,示例代码如下:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def perspective_thread(perspective_server: perspective.Server, tdengine_conn: taosws.Connection):
|
{{#include docs/examples/perspective/perspective_server.py:perspective_server}}
|
||||||
"""
|
```
|
||||||
Create a new Perspective table and update it with new data every 50ms
|
|
||||||
"""
|
|
||||||
# create a new Perspective table
|
|
||||||
client = perspective_server.new_local_client()
|
|
||||||
schema = {
|
|
||||||
"timestamp": datetime,
|
|
||||||
"location": str,
|
|
||||||
"groupid": int,
|
|
||||||
"current": float,
|
|
||||||
"voltage": int,
|
|
||||||
"phase": float,
|
|
||||||
}
|
|
||||||
# define the table schema
|
|
||||||
table = client.table(
|
|
||||||
schema,
|
|
||||||
limit=1000, # maximum number of rows in the table
|
|
||||||
name=PERSPECTIVE_TABLE_NAME, # table name. Use this with perspective-viewer on the client side
|
|
||||||
)
|
|
||||||
logger.info("Created new Perspective table")
|
|
||||||
|
|
||||||
# update with new data
|
|
||||||
def updater():
|
|
||||||
data = read_tdengine(tdengine_conn)
|
|
||||||
table.update(data)
|
|
||||||
logger.debug(f"Updated Perspective table: {len(data)} rows")
|
|
||||||
|
|
||||||
logger.info(f"Starting tornado ioloop update loop every {PERSPECTIVE_REFRESH_RATE} milliseconds")
|
|
||||||
# start the periodic callback to update the table data
|
|
||||||
callback = tornado.ioloop.PeriodicCallback(callback=updater, callback_time=PERSPECTIVE_REFRESH_RATE)
|
|
||||||
callback.start()
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTML 页面配置
|
### HTML 页面配置
|
||||||
|
|
||||||
|
@ -103,80 +74,7 @@ Python 连接器详细写入说明可参见 [Python 参数绑定](../../../refer
|
||||||
- 引入 Perspective 库,通过 WebSocket 连接到 Perspective 服务器,加载 meters_values 表来展示动态数据。
|
- 引入 Perspective 库,通过 WebSocket 连接到 Perspective 服务器,加载 meters_values 表来展示动态数据。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
{{#include docs/examples/perspective/prsp-viewer.html:perspective_viewer}}
|
||||||
|
|
||||||
// import the Perspective library
|
|
||||||
import perspective from "https://unpkg.com/@finos/perspective@3.1.3/dist/cdn/perspective.js";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async function () {
|
|
||||||
// an asynchronous function for loading the view
|
|
||||||
async function load_viewer(viewerId, config) {
|
|
||||||
try {
|
|
||||||
const table_name = "meters_values";
|
|
||||||
const viewer = document.getElementById(viewerId);
|
|
||||||
// connect Perspective WebSocket server
|
|
||||||
const websocket = await perspective.websocket("ws://localhost:8085/websocket");
|
|
||||||
// open server table
|
|
||||||
const server_table = await websocket.open_table(table_name);
|
|
||||||
// load the table into the view
|
|
||||||
await viewer.load(server_table);
|
|
||||||
// use view configuration
|
|
||||||
await viewer.restore(config);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to get data from ${table_name}, err: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// configuration of the view
|
|
||||||
const config = {
|
|
||||||
"version": "3.3.1", // Perspective library version (compatibility identifier)
|
|
||||||
"plugin": "Datagrid", // View mode: Datagrid (table) or D3FC (chart)
|
|
||||||
"plugin_config": { // Plugin-specific configuration
|
|
||||||
"columns": {
|
|
||||||
"current": {
|
|
||||||
"width": 150 // Column width in pixels
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"edit_mode": "READ_ONLY", // Edit mode: READ_ONLY (immutable) or EDIT (editable)
|
|
||||||
"scroll_lock": false // Whether to lock scroll position
|
|
||||||
},
|
|
||||||
"columns_config": {}, // Custom column configurations (colors, formatting, etc.)
|
|
||||||
"settings": true, // Whether to show settings panel (true/false)
|
|
||||||
"theme": "Power Meters", // Custom theme name (must be pre-defined)
|
|
||||||
"title": "Meters list data", // View title
|
|
||||||
"group_by": ["location", "groupid"], // Row grouping fields (equivalent to `row_pivots`)
|
|
||||||
"split_by": [], // Column grouping fields (equivalent to `column_pivots`)
|
|
||||||
"columns": [ // Columns to display (in order)
|
|
||||||
"timestamp",
|
|
||||||
"location",
|
|
||||||
"current",
|
|
||||||
"voltage",
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"filter": [], // Filter conditions (triplet format array)
|
|
||||||
"sort": [], // Sorting rules (format: [field, direction])
|
|
||||||
"expressions": {}, // Custom expressions (e.g., calculated columns)
|
|
||||||
"aggregates": { // Aggregation function configuration
|
|
||||||
"timestamp": "last", // Aggregation: last (takes the latest value)
|
|
||||||
"voltage": "last", // Aggregation: last
|
|
||||||
"phase": "last", // Aggregation: last
|
|
||||||
"current": "last" // Aggregation: last
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// load the first view
|
|
||||||
await load_viewer("prsp-viewer-1", config1);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- define the HTML Structure of the Dashboard -->
|
|
||||||
<div id="dashboard">
|
|
||||||
<div class="viewer-container">
|
|
||||||
<perspective-viewer id="prsp-viewer-1" theme="Pro Dark"></perspective-viewer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 参考资料
|
## 参考资料
|
||||||
|
|
Loading…
Reference in New Issue