所有文档

          物可视 IOTVIZ

          使用PlayerAPI更新数据

          在物可视编辑器中创建数据表时可以看到,在类型选项中可供选择的有“时序数据库”,“设备影子”,”静态数据“和”仿真数据“。

          image.png

          因此经常有用户咨询,物可视是不是不支持从用户 API 获取数据呢?

          好消息:物可视能够支持从用户 API 获取数据,具体来说,通过 PlayerAPI 来更新数据。

          下面就来演示具体的操作步骤。

          操作步骤

          1. 在物可视编辑器中,切换到”数据“标签,新建一个数据类型,类型选择静态数据。填入一个合适的名称,例如”温度数据“(这个很重要,后面会用到)。

            image.png

            文件类型(JSON/CSV)可以根据用户实际需求选择,这里我们假定选择 JSON。

            我们在这里只需要给出一个数据的结构即可,因此只需要一行数据。如果您想从文件中读取,则选择这个文件并上传;如果想简单些直接在下一步手工输入,也是可以的。这里我们假定选择直接在下一步手工输入,因此可以点击“下一步”。

          2. 第二步的重点是定义数据的结构,所以我们需要输入一行数据,从而让物可视了解数据表的列数,每列数据的名称,值的类型,初始值等信息。我们假定要开发一个“中国部分城市当前温度状态图”,那么在 JSON 文本中可以这样输入:

            [{
                "location": "上海",
                "temperature": 20
            },{
                "location": "北京",
                "temperature": 10
            },{
                "location": "广州",
                "temperature": 30
            },{
                "location": "四川",
                "temperature": 35
            }]

            image.png

            如果您在上一步选择了一个 JSON 文件,那么它的值会被自动填入到这里,您依然可以对它做修改。

          3. 数据准备好了,下一步是在画布上添加组件来展示这个数据。我们切换到“画布”标签,在组件中选择“统计柱状图”,拖动到页面上。

            image.png

          4. 接下来我们要把组件和数据建立联系。让图表组件处于选中状态。

            找到右侧面板中的“数据绑定”标签,并找到最后一项:“序列”。

            image.png

          5. 在弹出的“序列绑定”窗口中,首先选择数据表。这里会显示所有可用的数据表名称,选择我们刚才新建的“温度数据”。

            选中之后,我们会看到左侧会显示它的第一行数据,也就是我们刚才填入的 JSON。

            在右侧选择 X 轴和 Y 轴的显示内容。例如我们把城市 (location) 选成 X 轴,而把温度 (temperature) 选成 Y 轴。

            image.png

          6. 点击确定后,图表组件就会应用这个数据,从而变成我们预期中的“中国部分城市当前温度状态图”。

            image.png

            注意:X 轴和 Y 轴默认字体颜色是黑色,如果是深色背景,可能配置完成后会因为背景色而看不见。这并不是数据或者显示出错,不必惊慌。在图表组件的属性中,找到 X 轴和 Y 轴的“字体颜色”配置项,改为浅色即可。

          7. 返回到仪表盘列表页,先点击左下角的“发布”按钮,成功后,再点击“生成代码”。

            image.png

            我们会看到弹出了一个窗口,包含了引用代码。我们把这些代码复制下来,存放到本地的一个文件,并以 html 为扩展名。例如我们叫它 index.html。

          8. index.html 的内容大致是这样(具体的 dashboardId,token 等可能不同):

            <!DOCTYPE html>
            <html>
                <head>
                    <meta charset="utf-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1">
                    <meta name="renderer" content="webkit">
            
                    <!-- 加载 物可视SDK-->
                    <script type="text/javascript" src="https://iotviz.cdn.bcebos.com/bin/prod/sdk/bdiotvizplayer.min.js"></script>
            
                    <style>
                        html, body {
                            font-size: 12px;
                            font-family: "PingFang SC";
                            display: block;
                            margin: 0;
                            width: 100%;
                            height: 100%;
                        }
            
                        #content {
                            width: 100%;
                            height: 100%;
                        }
                    </style>
                </head>
            
                <body>
                    <div id="content"></div>
            
                    <script type="text/javascript">
                        const bdIotVizPlayer = window.BDIotVizPlayer;
                        const container = document.getElementById('content');
                        const myDashboard = bdIotVizPlayer({
                            containerElement: container,
                            dashboardId: '**********************',
                            fillMode: 'none', // <-- Optional, Possible value: 'none', 'contain', 'cover', 'responsive'
                            token: {
                                type: 'embedded', // <-- Must be 'embedded'
                                value: '**********************' // <-- Access Token for current dashboard
                            }
                        });
                        myDashboard.getDashboardConfig().then(function(config){
                            console.log(config); // <--- Current Dashboard Config
                        });
                        // 'myDashboard' exposes the API to the dashboard loaded
                        // refer to API doc for complete API description
                    </script>
                </body>
            </html>

            我们需要重点关注的是最后一小段,一会儿我们要在这里进行小幅修改。其他的内容只要维持原状即可。

            myDashboard.getDashboardConfig().then(function(config){
                console.log(config); // <--- Current Dashboard Config
            });
          9. 我们将使用 PlayerAPI 去更新这个数据表,首先我们得找到这个数据表。这时候,它的名字(“温度数据”)就起作用了,代码如下:

            // 只列出修改的代码,其余不变的部分不再重复
            myDashboard.getDashboardConfig().then(function(config){
                let dataTables = config.dataTables;
            
                // 寻找并赋值
                let temperatureData;
                dataTables.forEach(data => {
                    if (data.name === '温度数据') {
                        temperatureData = data;
                    }
                });
            });

            当然如果你确信仪表盘只有一个数据源,那你也可以直接用 let temperatureData = config.dataTables[0]; 进行赋值,就免去了一次循环查找。

          10. 使用 PlayerAPI 去更新静态数据,我们会用到 updateDataTableConfig 方法,代码如下:

            // 只列出修改的代码,其余不变的部分不再重复
            myDashboard.getDashboardConfig().then(function(config){
                let dataTables = config.dataTables;
            
                // 寻找并赋值
                let temperatureData;
                dataTables.forEach(data => {
                    if (data.name === '温度数据') {
                        temperatureData = data;
                    }
                });
            
                // 更新数据
                temperatureData.config.source = [{
                    location: '上海',
                    temperature: 22, // 原来是 20
                },{
                    location: '北京',
                    temperature: 15, // 原来是 10
                },{
                    location: '广州',
                    temperature: 29, // 原来是 30
                },{
                    location: '四川',
                    temperature: 31, // 原来是 35
                }];
                myDashboard.updateDataTableConfig(temperatureData.id, temperatureData.config).then(() => {
                    console.log('更新温度:', temperatureData.config.source);
                });
            });

            注意:这里更新的数据结构必须和 JSON 相同,如果有差异,更新将会被忽略。

          通过这个方法,我们可以对物可视发布后的仪表盘进行数据更新操作。既然控制权已经交到了用户手中,那么更新数据从哪里来,更新什么样的数据,什么时候更新数据等就全部由用户自由决定了。

          下面我们看两个非常典型的例子。

          模拟“真实”的假数据

          物可视是一个旨在降低 UI 编程成本的工具,除了使用它开发实际项目的可视化界面之外,也有相当一部分用户(尤其是商业推广方向的工作者)使用物可视来快速开发原型,用作项目的预期效果演示。对于这类用户,他们通常没有真实的数据源(例如时序数据库或者物影子等),或者操作这些的成本很大,因此多数使用静态数据来进行展示。可惜静态数据缺乏变化效果,因此演示用的仪表盘也是静态的,效果欠佳。

          在近期(2019.10)物可视升级增加了一种新的数据源类型:仿真数据源,它可以理解为一个“假的”数据源,通过预设的各种类型模拟出变化的数据,例如“在10-20之间随机变化的数字”。仿真数据可以让演示更加生动和真实。

          不过如果预设的变化无法满足个性化的需求时,我们就可以通过 PlayerAPI 来自己更新数据。毕竟控制权在自己手上,想生成怎样的数据都是你的自由。(需要少量 JavaScript 编程基础)

          继续使用上述“温度状态图”作为例子,我们可以这样来模拟数据:

          // 只列出修改的代码,其余不变的部分不再重复
          myDashboard.getDashboardConfig().then(function(config){
              let dataTables = config.dataTables;
          
              // 寻找并赋值
              let temperatureData;
              dataTables.forEach(data => {
                  if (data.name === '温度数据') {
                      temperatureData = data;
                  }
              });
          
              // 更新数据
              setInterval(() => {
                  temperatureData.config.source = [{
                      location: '上海',
                      temperature: getTemperature(20, 5), // 20 ± 2.5
                  },{
                      location: '北京',
                      temperature: getTemperature(10, 5), // 10 ± 2.5
                  },{
                      location: '广州',
                      temperature: getTemperature(30, 5), // 30 ± 2.5
                  },{
                      location: '四川',
                      temperature: getTemperature(35, 5), // 35 ± 2.5
                  }];
                  myDashboard.updateDataTableConfig(temperatureData.id, temperatureData.config).then(() => {
                      console.log('更新温度:', temperatureData.config.source);
                  });
              }, 3000);
          });
          
          function getTemperature(base, float) {
              return base + Math.round((Math.random() - 0.5) * float);
          }

          这样可以每隔 3 秒更新一次每隔城市的温度。温度在一个基准值附近,在浮动值的范围内随机变化。

          或者更进一步,我们还可以根据当前时间和过往上海的平均温度进行更加精细定制化的操作:

          // 只列出修改的代码,其余不变的部分不再重复
          function getTemperatureForShanghaiHai() {
              let month = (new Date()).getMonth() + 1;
          
              if (month >= 7 && month <= 9) { // 7, 8, 9
                  return getTemperature(37, 4);
              }
              if (month === 12 || month <= 2) { // 12, 1, 2
                  return getTemperature(0, 6);
              }
              if (month >= 3 && month <= 6) { // 3, 4, 5, 6
                  return getTemperature((month - 2) * 8, 8)
              }
              if (month === 10) {
                  return getTemperature(20, 10);
              }
              // 11
              return getTemperature(10, 10);
          
          }

          这就已经超越了仿真数据源的能力范围了,只有使用 PlayerAPI 更新数据才能做到。

          从用户 API 获取数据

          回到文章最开头的问题,究竟物可视如何支持从用户 API 获取数据呢?答案就是由用户代码发送请求,获取数据后使用 PlayerAPI 更新到图表。

          假设我们使用 axios 来发送请求,那么我们的 index.html 大致是下面这样:

          // 只列出修改的代码,其余不变的部分不再重复
          myDashboard.getDashboardConfig().then(function(config){
              let dataTables = config.dataTables;
          
              // 寻找并赋值
              let temperatureData;
              dataTables.forEach(data => {
                  if (data.name === '温度数据') {
                      temperatureData = data;
                  }
              });
          
              function startTimer() {
                  setTimeout(() => {
                      // 获取数据
                      getTemperatureData().then(data => {
                          temperatureData.config.source = data;
                          // 更新数据
                          return myDashboard.updateDataTableConfig(temperatureData.id, temperatureData.config);
                      }).catch(message => {
                          // 错误处理
                          console.log(message);
                      }).finally(() => {
                          // 下一个周期
                          startTimer();
                      })
                  }, 3000);
              }
          
              // 周期性执行任务
              startTimer();
          });
          
          function getTemperatureData() {
              return new Promise((resolve, reject) => {
                  axios.get('/some/api/to/get/temperature?time=now').then((response) => {
                      if (response.success) {
                          resolve(response.data); // response.data 的格式为 [{location: 'xxx', temperature: 0}, ...]
                      } else {
                          reject(response.message); // 失败时返回错误信息
                      }
                  });
              });
          }

          这里有几个注意点:

          1. 注意前端发送请求时的跨域问题。解决方法有两个:

            1. 将前端代码 (index.html) 和后端 API 部署在同一个域名+端口下;或者使用 nginx 为后端 API 进行代理,使之暴露到和前端同域。
            2. 为后端 API 开启 CORS,并将前端域名+端口设置为可信任的。
          2. 后端 API 返回的数据格式必须与图表组件需要的数据格式一致。如果不一致,需要一层转换(前后端均可),否则将无法生效。
          3. 可以使用任何允许发送 HTTP 请求的方法,例如原生的 fetch 或者其他类库替代例子中的 axios。
          4. 当 JavaScript 逻辑变多之后,可以独立成多个 JS 文件,也可以使用模块化管理(如 requireJS)甚至 webpack 等等,只要调用物可视仪表盘的核心代码不变即可。CSS 也是类似。
          5. 请求的 API 地址,请求的频率,响应的处理等均可自行决定,物可视不进行任何限制。

          为什么物可视不直接把用户 API 作为一种数据源?(扩展)

          本部分为扩展阅读,不影响功能的使用。如果您有兴趣,可以继续阅读,了解物可视的产品定位和设计思路。

          许多类似的可视化产品把用户 API 作为数据源的一种,和其他常见数据源(例如各类数据库,Elastic Search 等等)并列。当选择用户 API 时,还可以填入用户 API 的地址和更新频率,接着由可视化产品向 API 发送请求,自动更新。对用户来说,使用 API 和使用数据库一样,配置完成后自动运行,省时省力。

          那为什么物可视不采用这种做法呢?

          这其实和物可视的定位以及运行模式有关。

          其他可视化产品的思路

          熟悉其他可视化产品的用户应该能感受到,其他的可视化产品的最终产物是一个托管于提供方的 HTML 页面。当它作为一个整体来使用时确实省时省力,键入一串 URL 即可。这种思路的优势显而易见:

          1. 最大限度减少用户的操作复杂度,只需要输入 API 地址和更新频率等少量配置信息即可。
          2. 完全不需要用户拥有任何编程能力,也没有给用户任何写代码的空间。
          3. 产物(HTML)可直接使用。

          但它也有一定的缺点:

          缺点一:用户只能视其为一个黑盒来整体使用,而丧失了对内部细节的控制权。

          例如用户本身拥有一个可视化系统,那这两个可视化的产物就无法融合到一个页面上。顶多是开发一个公共页面,通过链接分别前往。但理想状态应当是在一个页面上同时显示两者,这对于黑盒来说是做不到的,更不谈两者可能还需要通信或者联动。

          正因为是个黑盒,于是所有信息必须提前配置完成,无法在运行时进行调整,所以自然就要求用户提前把 API 的相关信息配置完整。但这又引入了另外一些问题:

          缺点二:用户只能按照预设的选项来配置,无法自由发挥。

          例如可视化产品只开放用户填入 API 地址和更新频率,于是它就只能按照既定的频率去访问 API 更新数据,用户无法新增任何自定义逻辑。比如某 API 接口返回的数据变更不大,因此为了减少对 API 接口的压力,用户想在数据不变时延长更新间隔,而当数据变化时缩短间隔,这类“个性化”的逻辑就无法在固定的配置项中实现。

          缺点三:数据由可视化产品提供方获取并更新到界面上,因此数据其实到外部走了一圈,数据安全有隐患。

          天工物可视的思路

          天工物可视的定位旨在降低用户开发 UI 界面的成本,产出既支持黑盒的 HTML(供用户快速使用),也支持以代码形式部分嵌入到用户自己的系统中。对于有个性化需求又有一定编程基础的用户来说,代码形式嵌入是最好的选择:既满足了使用方便,又拥有相当的控制权。

          另一方面,在物联网环境下,可视化产品的用户多数是一些中小型的企业。这些企业或多或少都有自己的开发人员,且不论水平高低,最最基础的编程能力是一定具备的,这也是天工物可视额外支持代码嵌入的初衷。

          在物可视的思路中,物可视的产物只是页面中的显示部分。而页面的其他部分(如业务逻辑,获取数据,错误处理等)均应由更加熟悉自身产品的用户全权控制。我们可以把物可视的产物想象成 HTML 中的 <img> 标签,只需要告诉它图片地址,它就能绘出图片。如何把地址转换为图片绘制到页面上用户不必关心,而如何获取图片地址,以什么频率更换图片地址等问题物可视也不关心。

          image.png

          (标红的是用户控制部分)

          在这样的定位和思路下,造就了物可视和其他可视化产品不同的数据通路。按照这个设计,如上的三个缺点都能一并迎刃而解:

          1. 物可视的产物只是页面的一部分(确切的说是一个容器 div 节点加一段启动 JS 代码),用户仍有其他所有内容的控制权,可以编写自定义 JS 做任何事。
          2. 既然能够编写自定义 JS 做任何事,那么可变更的 API 地址,可变更的更新频率等任何特殊需求都能够满足。
          3. 数据由用户的 JS 直接从用户的 API 上获取,不经过百度天工的任何服务器,安全可靠。(除非数据源是天工产品,例如 TSDB 时序数据库)

          两种思路的比较

          这两种思路只是看问题的不同角度,并不存在优劣高低之分,只有适合与不适合之分。我们暂且把其他可视化产品的思路称为“黑盒 HTML 模式”,而物可视的思路称为“代码嵌入模式”。那么它们分别适合于什么样的使用者呢?

          1. 黑盒 HTML 模式:

            • 适合真正的编码零基础用户
            • 适合时间紧迫,需要快速构建产品的用户
            • 适合需要开发基于演示的示例产品的用户
            • 适合没有过多个性化需求,且对数据安全性不太敏感的场景
          2. 代码嵌入模式:

            • 适合有一定编码基础的用户(相关专业的应届大学毕业生水平即可)
            • 适合有个性化需求,或对数据安全性极其敏感的场景

          最后一点需要说明,那就是天工物可视其实同时支持两种模式。使用发布后的仪表盘的分享链接打开即为“黑盒 HTML 模式”,只不过因为没有显式的 用户 API 数据源可供选择,因此在这种模式下,的确不支持从用户 API 获取数据,这点还请使用者多多注意。

          上一篇
          数据处理实践
          下一篇
          离线模式实践