跳到主要内容
版本:6.0.0

自定义图表

1. 自定义图表概述

自定义图表,是观远数据提供的一种开放式的可视化分析图表类型。用户可以基于观远后台强大的数据处理能力,再借助第三方的可视化能力,构建出自定义的数据可视化展现方式。使用自定义图表,可以让平台的可视化能力不局限于当前已有的图表类型,可以根据自己的实际场景,扩展出丰富多样的可视化类型。

观远数据支持两种不同的图表可视化编辑方式:「自定义图表」和「自定义图表 Lite」。

2. 自定义图表开发说明

创建自定义图表,需要使用到观远提供的可视化SDK扩展包,以及各类可视化图表库的使用,涉及到 JavaScript 的开发,需要用户有一定的前端开发能力。

观远数据自定义图表插件开发和打包的代码仓库地址:https://github.com/GuandataOSS/visualization-tool。您可以根据需要进行使用与扩展。

3. 自定义图表使用介绍

创建自定义图表,主要分为两大步骤:

  • 数据准备

  • 制作图表

3.1 数据准备

点击仪表板右上角的新建卡片,选择「自定义图表」,选择一个数据集。

1.png

2.png

进入自定义图表编辑页后,可以看到编辑页主要分为三个区域:数据视图列表、当前数据视图的编辑区、图表视图。

数据准备过程,与平常做可视化分析类似。可以在数据视图编辑区内,拖拽相应的维度和数值到对应的区域,准备好用来绘制图表的数据。

不同之处在于:自定义图表的数据,可来自多个数据视图(一个视图类似于一个卡片计算)。多个视图的数据信息将会以数组的形式,供给图表视图绘图所用。

3.2 制作图表

数据准备好后,我们切换到「图表视图」,可选择两种不同的图表可视化编辑方式,这里选择第一种「自定义图表」。

3.png

3.2.1 界面介绍

(1)各区域介绍

a. 自定义图表/自定义Lite图表:两种自定义图表编辑方式

b. 运行按钮:用于代码编写完成后,在「图表预览区」查看展示效果

c. 查看视图数据结构:展示从「数据视图」中获取的数据

d. HTML 代码区:代码编辑区,下文将详细介绍

e. CSS 代码区:代码编辑区,下文将详细介绍

f. JAVASCRIPT 代码区:代码编辑区,下文将详细介绍

g. 图表预览区

9.png

(2)代码编辑区

「自定义图表」的代码编辑区包括:HTML 代码区、CSS 代码区、JAVASCRIPT 代码区。

如下图所示,当我们首次进入「图表视图」时,代码编辑区内会存在一些默认代码,而且在图表预览区,会以表格形式展示「数据视图」的数据。同时,每个区域支持拖动和最大化。

10.png

  • HTML 代码区:

主要用于定义图表渲染的容器以及图表渲染时需要引用的图表库;

11.png

  • CSS 代码区:

静态样式设置。一般情况下不需要进行额外修改,容器的自适应可在此处进行设置。不支持 SCSS/LESS 这类预处理类语言,只支持原生 CSS;

  • JAVASCRIPT 代码区:

主要用于图表数据的接入与具体渲染动作设置;

如下,在书写或者替换 JAVASCRIPT 中的内容时,需要书写在 function renderChart 下的「/* ------ Custom Code Start ------ /」与「/ ------ Custom Code End ------ */」中间。

// 观远自带的渲染函数,一般不用去修改
function renderChart (data, clickFunc, config) {
/* ------ Custom Code Start ------ */
// 这里是你的代码书写位置
/* ------ Custom Code End ------ */
}
// 观远的插件代码,用于控制最终的图表渲染
new GDPlugin().init(renderChart)

从5.6.0 版本开始, renderChart函数新增utils 函数,支持调用utils.refreshData() 来触发卡片数据刷新(会从后端服务器中重新获取数据)。 

image-2023-03-08-09-40-11-205.png

若期望传入参数不满足条件时在图表区域进行提示,则可以按照如下方式自定义提示:

function renderChart (data, clickFunc, config) {
   if(data[0].length === 0) {
       const dom = document.getElementById('container')
       dom.innerHTML = "
数据为空"
       return
   }
}

3.2.2 可访问的数据和函数

javascript
function renderChart (data, clickFunc,config) {
   /* ------ Custom Code Start ------ */
   // ...custom code for render chart
   /* ------ Custom Code End ------ */
}
new GDPlugin().init(renderChart)

该部分代码跟SDK相关的部分说明如下:

GDPlugin

GDPlugin类,负责数据通讯及调用图表绘制方法;

init

init,接收一个renderChart函数,GDPlugin 会在接收到数据后调用 renderChart, 提供三个入参(data, clickFunc,config);

data

data,指「数据视图」中所选字段对应的数据。可通过「查看视图数据结构」,预览具体数据。格式如下:

[
 [
   {
     "name": "大区",
     "numberFormat": null,
     "data": [
       "其它",
       "华东",
       "西南",
       "华南",
       "华中",
       "华北"
     ]
   },
   {
     "name": "销售金额",
     "numberFormat": null,
     "data": [
       4916842,
       4290262,
       3157709.5,
       3037943.5,
       2167815,
       858516
     ]
   }
 ]
]

clickFunc

clickFunc,接受一个参数,用来支持自定义图表发起的联动、跳转等交互信息。格式如下:

function clickFunc(data) {}
// data 的格式如下:
{
clickedItems: [ {
  idx: [ 1, 2 ], // 列的index路径。eg. 第二个视图数据集的第三列 [ 1, 2 ]
   colName: 'xxx', // 列名称
   value: [ 'xxx', 'yyy' ], // 值格式
 } ]
}

config

config,图表属性设置,目前包含 theme, colors, customOptions, language 四个属性;

const { theme, colors, customOptions, language } = config
  • theme:当前系统主题色,对应的值包含「LIGHT」和「DARK」。

  • colors:主题颜色,在「数据视图」右侧可配置,对应一个数组,格式如下:

[
 "#4379CE",
 "#99B8F1",
 "#FADB37",
 "#FC8602",
 "#FFA748",
 "#72B5EB",
 "#78CDED",
 "#AC9AE2",
 "#FAC36F",
 "#FD7F7F",
]

8.jpg

  • customOptions:自定义配置化设置,支持自定义图表的配置化,方便插件安装后更好的对图表进行设置。
// customOptions 为一个对象,字段可根据图表去自行定义,然后使用
{
 showLabel: false,
 fontSize: 12,
}
  • language:当前系统使用语言(5.6.0 版本支持)。

3.2.3 联动交互

自定义图表可以用 highcharts 实现带联动能力的蝴蝶图。代码如下:

javajavascript
const DEDAULT_COLORS = ['#2f7ed8', '#f28f43', '#1aadce', '#492970', '#f28f43', '#77a1e5', '#c42525', '#a6c96a']
const DEFALUT_THEME = 'LIGHT'
const chart = Highcharts.chart("container", {
   chart: {
       type: "bar"
   },
   title: {
       text: null
   },
   xAxis: [
       {
           categories: [],
           reversed: false,
           labels: {
               step: 1
           }
       }
   ],
   yAxis: {
       title: {
           text: null
       }
   },
   plotOptions: {
       series: {
           stacking: "normal",
       }
   },
   series: []
});
function renderChart(data, clickFunc, config) {
  /* ------ Custom Code Start ------ */
   if (!data || data[0].length < 3) {
       const dom = document.getElementById('container')
       dom.innerHTML = ""
       return
   }
   const { theme = DEFALUT_THEME, colors = DEDAULT_COLORS } = config || {}
   const isDarkTheme = theme !== 'LIGHT'
   chart.update(
       {
           colors,
           xAxis: [
               {
                   categories: data[0][0].data,
                   title: data[0][0].name,
                   lineColor: isDarkTheme ? '#B4C2D6' : '#DBDFE7',
                   labels: {
                       style: {
                           color: isDarkTheme ? '#B4C2D6' : '#617282',
                       }
                   }
               }
           ],
           yAxis: {
               gridLineColor: isDarkTheme ? '#B4C2D6' : '#DBDFE7',
               labels: {
                   style: {
                       color: isDarkTheme ? '#B4C2D6' : '#617282',
                   }
               }
           },
           legend: {
               itemStyle: {
                   color: isDarkTheme ? '#B4C2D6' : '#617282',
               },
           },
           plotOptions: {
               series: {
                   stacking: "normal",
                   events: {
                       click: (event) => {
                         /* 从图表的事件到clikcFunc的调用示例 */
                           const { category } = event.point
                           if (category) {
                               const attrCol = data[0][0]
                               clickFunc({
                                   clickedItems: [
                                   {
                                       idx: [ 0, 0 ],
                                       colName: attrCol.name,
                                       value: [ category ],
                                   }
                               ]
                               })
                           }
                       }
                   }
               }
           },
       },
       false
   );
   const length = chart.series.length;
   Array.from({ length }).forEach((_, index) => {
       chart.series[length - index - 1].remove();
   });
   data[0].slice(1).forEach(function(serie, index) {
       if (index === 0) {
           chart.addSeries(
               { ...serie, data: serie.data.map(item => -item) },
               false
           );
       } else {
           chart.addSeries(serie, false);
       }
   });
   chart.redraw();
}
new GDPlugin().init(renderChart);

3.2.4 使用数据格式

由于数据格式的配置是基于 d3-format 的,所以实际使用时需要先导入 d3-format,然后调用 d3-format 提供的方法进行格式化。

引入d3-format 对应的在线地址:

image.png

定义格式化工具函数:

const isEnLang= (currentLang) => {
   return currentLang === 'en-US' || currentLang === 'en'
}
var formatCondenseNumber = (value, digits = 2, prefix = '', suffix = '', lang = '') => {
   const isPositive = value > 0
   const absVal = Math.abs(value)
   digits = Math.min(digits, 6)
   let [ num, unit ] = [ null, '' ]
   let [ nums, units ] = [ [], [] ]
   if (isEnLang(lang)) {
       // English, K M B T
       nums = [ 1000, 1000 * 1000 ]
       units = [ 'K', 'M' ]
   } else {
       // Chinese 万 亿 兆
       nums = [ 10000, 10000 * 10000, 10000 * 10000 * 10000 ]
       units = [ '万', '亿', '兆' ]
   }
   let i = nums.length - 1
   for (; i >= 0; i -= 1) {
       const base = nums[i]
       if (absVal >= base) {
           num = (absVal / base)
           num = Number(num).toFixed(digits)
           unit = units[i]
           break
       }
   }
   if (i < 0) {
       num = Number(absVal).toFixed(digits)
   }
   const signSymbol = (isPositive || absVal === 0) ? '' : '-'
   return `${signSymbol}${prefix}${num}${unit}${suffix}`
}
var genFakeValue = (value, decimalPlaces) => {
   if (typeof value !== 'number') return value
   if (decimalPlaces === 0) return value
   if (value.toString().indexOf('.') === -1) return value
   const [ integer, decimal ] = value.toString().split('.')
   const digits = decimal.length
   if (digits - decimalPlaces !== 1) return value
   const fakeDecimal = value > 0 ? +`0.${decimal}01` : -`0.${decimal}01`
   return +integer + fakeDecimal
}
var numberFormatFn = (params, lang) => {
   const { value, format } = params
   const { specifier, locale, prefixUnit = '', suffix = '', divideDataBy = 1, decimalPlaces = 0 } = format || {}
   const newSuffix = prefixUnit + suffix
   const fakeValue = genFakeValue(value, decimalPlaces)
   if (format && format.isAuto) {
       const prefix = locale?.currency?.[0] || ''
       return formatCondenseNumber(fakeValue, decimalPlaces, prefix, suffix, lang)
   }
   if (typeof specifier !== 'string') {
       return fakeValue + newSuffix
   }
   const fakeDividedValue = genFakeValue(fakeValue / divideDataBy, decimalPlaces)
   if(locale) {
     const defaultLocale = {
         decimal: '.',
         thousands: ',',
         grouping: [ 3 ],
     }
     return `${d3.formatLocale({ ...defaultLocale, ...locale}).format(specifier)(fakeDividedValue)}${newSuffix || ''}`
   }
   return `${d3.format(specifier)(fakeDividedValue)}${newSuffix || ''}`
}

使用上述定义的格式化工具函数格式化数值:

// 以原有绘制表格的模板代码为例(renderChart 的第三个参数 config 中传递了 language)
var formatSetting = singleData[k].numberFormat
var singleValue = singleData[k].data[j]
if(formatSetting) {
   singleValue = numberFormatFn(
     { value: Number(singleValue), format: formatSetting },
     config?.language || '',
   )
}
var row = ''' + singleValue  + '

可以看到对应效果如下:

1.png

3.3 绘制示例

以下步骤,为大家介绍如何通过 「自定义图表」 一步步绘制一个「南丁格尔玫瑰图 」。

Step1. 数据准备

点击仪表板右上角的「新建卡片」,选择「自定义图表」,选择一个数据集。

1.png

2.png

进入自定义图表编辑页,在「数据视图」中选择数据,此处我们选择「大区」和「销售金额」两个字段。

3.png

切换视图,点击页面顶端,从「数据视图」切换到「图表视图」。

4.png

Step2:渲染函数的入参

数据接入后,我们需要对这些数据进行渲染。

在「图表视图」中,点击右上角的「查看视图数据结构」,可以预览从「数据视图」中传入的数据。

5.png

接下来,我们要对「图表视图」中不同模块的代码数据进行替换,具体代码如下:

(1)HTML:主要用于定义图表渲染时需要引用的图表库

如下,输入需要用到的图表绘制的 CDN 链接库

7.png

(2)CSS:主要用于样式设置(不支持 SCSS/LESS 这类预处理类语言,只支持原生 CSS)

#container {
   width: 100%;
   height: 100%;
   overflow: hidden;
}

(3)JAVASCRIPT:主要用于图表数据的接入与具体渲染动作设置

const myChart = echarts.init(document.getElementById('container'))

const customOptions = {
   showToolbox: true
}

function renderChart (data, clickFunc, config) {
   /* ------ Custom Code Start ------ */
   // 读取配置
   const { theme, colors } = config
   const isDarkTheme = theme === 'DARK'
   // 配置化使用默认配置,后期接入
   const { showToolbox } = customOptions
   // 使用视图数据集1的数据
   const seriesData = data[0]
   const [ names, values ] = [ seriesData[0].data, seriesData[1].data ]
   const option = {
       legend: {
           top: 'bottom',
           textStyle: {
               color: isDarkTheme ? '#D1D8E3' : '#343D50',
           }
       },
       toolbox: {
           show: showToolbox,
           feature: {
               mark: { show: true },
               dataView: { show: true, readOnly: false },
               restore: { show: true },
               saveAsImage: { show: true }
           }
       },
       series: [
           {
               name: 'Nightingale Chart',
               type: 'pie',
               radius: [ 50, 250 ],
               center: [ '50%', '50%' ],
               roseType: 'area',
               itemStyle: {
                   borderRadius: 8
               },
               data: names.map((name, index) => ({
                   name,
                   value: values[index],
               })),
           }
       ],
       color: colors,
       label: {
           color: isDarkTheme ? '#D1D8E3' : '#343D50',
       },
   };
   myChart.setOption(option)
   myChart.on('click', function (params) {
       const { name, seriesIndex } = params
       clickFunc({
           clickedItems: [
               {
                   idx: [ 0, seriesIndex ],
                   colName: seriesData[0].name,
                   value: [ name ],
               }
           ]
       })
   });
   myChart.resize()
   /* ------ Custom Code End ------ */
}
new GDPlugin().init(renderChart)

Step3:成果展示

全部替换完成后,点击「运行」,即可完成「南丁格尔玫瑰图」的制作,展示效果如下:

6.png

4. 自定义图表 Lite 使用介绍

自定义图表 Lite(自定义图表轻量版)是基于自定义图表的一种可视化编辑方式,与自定义图表界面相比,自定义图表 Lite 只保留 Java Script 和图表预览区,Java Script 区只负责生成对应的 Echarts option 即可。

自定义图表 Lite 绘制图表,对用户的开发能力要求相对较低,用户可直接在 Echarts 官网中选取图表并在BI平台中绘制出该图。

4.1 数据准备

「自定义图表 Lite」的数据准备过程与「自定义图表」一致,具体操作见上文 3.1 数据准备。下面以 ECharts 为例,创建一个自定义可视化图表。

4.2 制作图表

数据准备好后,我们切换到「图表视图」,可选择两种不同的图表可视化编辑方式,这里选择第二种「自定义图表 Lite」。

4.png

4.2.1 界面介绍

自定义图表 Lite 视图页分为两个区域,分别是:

  • 左:JavaScript代码区

  • 右:图表预览区

如上图所示,只需要在 Java Script 代码区编写生成 option 的代码,然后点击运行,即可预览代码对应的图表。

4.2.2 基于Echarts官方示例快速绘制

Echartshttps://echarts.apache.org/examples/zh/index.html)官方示例上,查找希望绘制的图表样式。下面以绘制一个「基础面积图」为例。

a. 点击进入该示例的详情页。

8.png

b. 拷贝左侧代码编辑区域里已有的 option。

9.png

对应代码如下:

option = {
 xAxis: {
   type: 'category',
   boundaryGap: false,
   data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
 },
 yAxis: {
   type: 'value'
 },
 series: [
   {
     data: [820, 932, 901, 934, 1290, 1330, 1320],
     type: 'line',
     areaStyle: {}
   }
 ]
};

c. 将拷贝的 option 粘贴到自定义图表 Lite 的编辑界面,点击运行,即可绘制出该图。

10.png

11.png

上述图表中的展示数据,使用的是 ECharts 官方提供的模拟数据。若想要替换为 BI 平台内部数据,需要我们在「数据视图」中进行数据选取和配置(详见 4.2.3 的 data )。

4.2.3 可访问的数据和函数

Javascript 代码区域的代码是运行生成实际绘图使用的 option 函数的函数体的一部分,可以直接访问 data 、config、clickFunc 等变量,注意不要覆盖。

data

data,为「数据视图」步骤中所选择的数据集字段对应的数据,可通过「查看视图数据结」”来查看选中的字段对应的数据。

type IData = Array < Array < {
   name: string, // 字段名称
  data: Array, // 该字段对应的数据
}>>

config

interface IConfig {
    colors: string[], // “数据视图”中选中的图表主题对应的颜色
    customOptions: object, // 自定义图表属性配置项
    theme: 'LIGHT' | 'DARK', // 当前系统主题色
    language: //当前系统使用语言,5.6.0 支持
}

clickFunc

clickFunc,接受一个参数,用来支持自定义图表发起的联动、跳转等交互信息。

type IClickedItem = {
    idx: number[] // 列的查找路径。eg. 第二个数据集的第三列 [ 1, 2 ]
    colName: string // 列名称
    value: string[] // 值
}
type IClickFunc = (params: { clickedItems: IClickedItem[] }) => void

utils

  • 提供了一些工具函数

    • numberFormat:使用 d3-format 对数据进行格式化

    • getChartInstance : 用于获取 ECharts 图表实例

type IFieldNumberFormat = {
    divideDataBy?: number,
    excelFormat?: string,
    prefixUnit?: string,
    specifier: string,
    suffix?: string,
    locale?: object,
    decimalPlaces?: number,
    isAuto?: boolean,
}
type IUtils = {
    numberFormat: ({ value: number, format?: IFieldNumberFormat }) => any,
    getChartInstance: () => any 
}

window

对当前浏览器环境下的 window 进行了裁剪,无法访问 localStorage、indexedDB 等字段。

4.2.4 联动交互

前文我们提到,JavaScript 代码区域可使用 clickFunc  和 getChartInstance 这两个方法,接下来将使用这两个方法来实现点击触发联动的交互。

输入代码:

option = {
   xAxis: {
       type: 'category',
       data: data[0][0].data,
   },
   yAxis: {
       type: 'value'
   },
   series: [
       {
           data: data[0][1].data,
           type: 'line',
       }
   ]
}
// 获取 ECharts 图表实例
const chartInstance = utils.getChartInstance()
if(chartInstance) {
   // 移除所有点击事件监听
   chartInstance.off('click')
   // 点击事件监听
   chartInstance.on('click', (params) => {
       // 根据点击事件的参数组装 clickedItems
       const clickedItems = [{
           idx: [ 0, 0],
           name: data[0][0].name,
           value: [ params.name ],
       }]
       // 可以拿到 clickFunc 则调用 clickFunc
       if(clickFunc) {
           clickFunc({ clickedItems })
       }
   })
}

点击保存后退出,在卡片上进行联动关系的配置后,即可触发联动的交互。

12.png

4.2.5 使用数据格式

首先,在「数据视图」设置数值字段的数据格式,可从表格中查看数据的格式化结果。

13.png

其次,在「图表视图」中可查看视图数据结构,可以看到当前数据格式设置对应的配置。

14.png

使用提供的工具函数 utils.numberFormat 即可使用当前数据格式的设置对对应数据进行格式化。

对应代码如下:

option = {
   xAxis: {
       type: 'category',
       data: data[0][0].data,
   },
   yAxis: {
       type: 'value',
       axisLabel: {
           formatter: (value) => {
               // 读取数据格式配置
               const formatDeFn = data[0][1].numberFormat
               if(formatDeFn) {
                   // 调用工具函数对数据进行格式化
                   return utils.numberFormat({ value, format: formatDeFn })
               }
               return value
           }
       }
   },
   series: [
       {
           data: data[0][1].data,
           type: 'line',
       }
   ]
}

4.2.6 ECharts 的配置属性

由于 ECharts 有默认的样式配置,简单的使用可通过设置坐标轴(xAxis 和 yAxis)以及系列(series)即可。

若需要对样式进行优化,则需要根据配置教程提供的属性来设置,下面给出部分设置的解释。

  • legend: 图例

    • left、top、right、bottom : 设置图例离容器的距离

    grid:绘图网格,当需要基于多个网格绘制时就需要针对每个图设置绘图网格的属性

    • left 、top、right、bottom:设置绘图网格离容器的距离

    • width、height: 设置绘图网格的宽高

    • containLable:grid 区域是否包含坐标轴的刻度标签,为 true 时常用于「防止标签溢出」

    xAxis/yAxis : grid 的 x 轴 / y轴

    • gridIndex: 所在的 grid 的索引,多个 grid 时需要设置

    • axisLabel

      • rotate:刻度标签的旋转角度,显示不下时可以设置角度防止标签重叠

      • hideOverlap: 隐藏重叠的标签

      boundaryGap: 坐标轴两边的留白策略,注意类目轴和非类目轴表现不一致。且在设置了 min 和 max 时无效

      • 对于非类目轴,boundaryGap 需要是一个包含两个值的数组,值分别代表数据最小值和最大值的延伸范围,即设置 boundaryGap 为[ 0.5, 1], 数据的范围为 [ 50, 120] ,则最终的范围为 [ 50 - 0.5 * 50, 120 + 1 * 120],即为 [ 25, 240]

    toolbox:工具栏,内置有「导出图片」、「数据区域缩放」等工具

4.3 绘制示例

以下步骤,为大家介绍如何通过 「自定义图表Lite」 一步步绘制一个「南丁格尔玫瑰图 」。

Step1. 数据准备

点击仪表板右上角的「新建卡片」,选择「自定义图表」,选择一个数据集。

1.png

进入自定义图表编辑页,在「数据视图」中选择数据,此处我们选择「大区」和「销售金额」两个字段。

15.png

切换视图,点击页面顶端,从「数据视图」切换到「图表视图」。

4.png

左上角选择「自定义图表Lite」,在弹窗中,点击「切换」,即可进入「自定义图表 Lite 」模式。

16.png

17.png

Step2:制作图表

在「自定义图表 Lite 」模式中,界面右侧默认展示一个 mock 数据的折线图,接下来我们要将其替换成我们期望的图表和数据。

打开南丁格尔玫瑰图的官方示例,拷贝左侧对应的 option 到 BI 平台的图表编辑区域。

19.png

点击「运行」,展示效果如下:

18.png

此时图表区域数据仍然是模拟数据,我们需要将数据替换为「数据视图」中选择的数据,所以还需要修改左侧的  JAVASCRIPT 代码,示例代码如下:

// 拼装得到对应的数据
const sData = data[0][0].data
   .map((name, index) => ({ name, value: data[0][1].data[index] }))
   .sort((prev, cur) => (cur.value - prev.value))
option = {
   legend: {
       bottom: 60, // 设置图例位置
   },
   series: [
   {
       name: 'Nightingale Chart',
       type: 'pie',
       radius: [50, 250],
       center: ['50%', '50%'],
       roseType: 'area',
   itemStyle: {
       borderRadius: 8
   },
   data: sData, // 设置数据
   }
 ]
};

Step3:成果展示

修改完成后,点击「运行」,即可完成「南丁格尔玫瑰图」的制作,展示效果如下:

20.png

5. 更多样例代码

下面以antv-g2的动态图表为例,简要说明如何结合第三方图表库和现有的数据集生成一个酷炫的图表。

(1) 首先,我们要在页面内引入第三方可视化库。我们需要找到类似cdn或者其他在线资源,在HTML区块中通过1.jpg引入。具体操作详情参照antv-g2的浏览器引入方式

(2)将该图表JavaScript实现部分的代码移植入“Custome Code Start”与“Custom Code End”区间内,点击“运行”便可将图形原模原样搬入BI平台。

(3)可以看到该图中一共需要三种数据(省份、年份、数值),在数据视图中我们也需要在新增2个维度,1个数值(必要时可以筛选排序)。步骤2中,我们已经准备了一份“日期(季度)+大区+销售金额”的数据。这时候我们要将renderChart传入的数据data转化成antv-g2的动态图表需要的数据结构。

(4)接着再根据自己的需要,对图表配置进行相关调整,一张酷炫的图表就生成了。

JavaScript代码最后一行 `new GDPlugin().init(renderChart)` ,会保证在每次数据变更时,重新执行renderChart。所以建议不要把new chart这类一次性构造,持续使用的声明放在renderChart里面。

下面,我们给出以上“条形滚动轮播图”用AntV G2、Highcharts、eCharts三种方式分别实现样例代码供大家参考。

5.1 AntV G2 实现

a. HTML代码块

image.png

b. CSS代码块

#container {
   width: 100%;
   height: 100%;
   overflow: auto;
   font-size: 12px;
   color: #343d50;
}
.replay {
   position: fixed;
   top: 10px;
   right: 10px;
}

c. Javascript代码块

javascript
var config = {
 loop: false, // 是否循环播放 false: 不循环播放 true: 循环播放,(布尔值,无需引号包裹)
 interval: 1200, // 轮播间隔时间 默认为1200ms, 小于1000ms 以1000ms计, (数值,无需引号包裹)
 sort: 'asc', // 图排序方式 asc: 升序  desc: 降序(默认) 以左下角为原点,(字符串,需用英文引号包裹)
 type: 'bar', // bar: 条形图  column: 柱状图,(字符串,需用英文引号包裹)
 showCount: Infinity, // Infinity: 显示全部(默认),为数字时如10,表示图表中最大显示10个分类,(数值,无需引号包裹)
 textStyle: { // 动态维度
   fontSize: 40, // 字体大小,(数值,无需引号包裹)
   fontWeight: 'bold', // 字体粗细 bold: 加粗(默认) normal: 不加粗,(字符串,需用英文引号包裹)
   color: '#ddd', // 字体颜色,(字符串,需用英文引号包裹)
 },
 padding: [ 20, 60, 20, 60 ], // 图表区域距离容器边缘的上 右 下 左 间距,主要用来腾出区域给横轴/纵轴的分类标签
 colors10: [
   '#5B8FF9',
   '#5AD8A6',
   '#5D7092',
   '#F6BD16',
   '#E86452',
   '#6DC8EC',
   '#945FB9',
   '#FF9845',
   '#1E9493',
   '#FF99C3',
 ], // 分类颜色色板,分类个数小于等于 10 时使用
 colors20: [
   '#5B8FF9',
   '#CDDDFD',
   '#5AD8A6',
   '#CDF3E4',
   '#5D7092',
   '#CED4DE',
   '#F6BD16',
   '#FCEBB9',
   '#E86452',
   '#F8D0CB',
   '#6DC8EC',
   '#D3EEF9',
   '#945FB9',
   '#DECFEA',
   '#FF9845',
   '#FFE0C7',
   '#1E9493',
   '#BBDEDE',
   '#FF99C3',
   '#FFE0ED',
 ], // 分类颜色色板,分类个数小于 20 时使用
}
var sortIsDesc = config.sort === 'desc'
var typeIsBar = config.type === 'bar'
var Chart = G2.Chart
var registerAnimation = G2.registerAnimation
registerAnimation('label-appear', function (element, animateCfg, cfg) {
 var label = element.getChildren()[0];
 var coordinate = cfg.coordinate;
 var startX = coordinate.start.x;
 var finalX = label.attr('x');
 var labelContent = label.attr('text');
 label.attr('x', startX);
 label.attr('text', 0);
 var distance = finalX - startX;
 label.animate(function (ratio) {
   var position = startX + distance * ratio;
   var text = (labelContent * ratio).toFixed(0);
   return {
     x: position,
     text: text,
   };
 }, animateCfg);
});
registerAnimation('label-update', function (element, animateCfg, cfg) {
 var startX = element.attr('x');
 var startY = element.attr('y');
 var finalX = cfg.toAttrs.x;
 var finalY = cfg.toAttrs.y;
 var labelContent = element.attr('text');
 // @ts-ignore
 var finalContent = cfg.toAttrs.text;
 var distanceX = finalX - startX;
 var distanceY = finalY - startY;
 var numberDiff = +finalContent - +labelContent;
 element.animate(function (ratio) {
   var positionX = startX + distanceX * ratio;
   var positionY = startY + distanceY * ratio;
   var text = (+labelContent + numberDiff * ratio).toFixed(0);
   return {
     x: positionX,
     y: positionY,
     text: text,
   };
 }, animateCfg);
});
function transformData (data) {
   var result = {}
   var q = data[0]
   var zones = data[1]
   var values = data[2]
   q.data.forEach(function (item, index) {
       if (result[item]) {
           result[item].push({ value: values.data[index], zone: zones.data[index] })
       } else { result[item] = [{ value: values.data[index], zone: zones.data[index] }] }
   })
   return result
}
function handleData(source) {
 source.sort(function (a, b) {
   if (sortIsDesc) {
     return b.value - a.value;
   } else {
     return a.value - b.value
   }
 });
 return source;
}
function setAnnotation (chart, content) {
 var position = ['90%', '90%']
 var textAlign = 'start'
 switch (true) {
   case typeIsBar && !sortIsDesc:
     position = ['95%', '90%']
     textAlign = 'end'
     break
   case !typeIsBar && !sortIsDesc:
     position = ['5%', '10%']
     break
   case !typeIsBar && sortIsDesc:
     position = ['95%', '10%']
     textAlign = 'end'
     break
   case typeIsBar && sortIsDesc:
   default:
     position = ['95%', '10%']
     textAlign = 'end'
 }
 config.textStyle.textAlign = textAlign
 chart.annotation().text({
   position: position,
   content: content,
   style: config.textStyle,
   animate: false,
 });
}
var $replay = document.querySelector('.replay')
var chart = new Chart({
 container: 'container',
 autoFit: true,
 height: 500,
 padding: config.padding,
});
var interval
function countUp() {
 var dataSource = countUp.dataSource
 var count = countUp.count
 var data = handleData(Object.values(dataSource)[count]).slice(0, config.showCount)
 var colors = data.length <= 10 ? config.colors10 : config.colors20
 var text = Object.keys(dataSource)[count]
 if (count === 0 && !countUp.once) {
   chart.data(data);
   // 水平柱状图 or 垂直柱状图
   if (typeIsBar) {
     chart.coordinate('rect').transpose();
   }
   chart.legend(false);
   chart.tooltip(false);
   chart.axis('zone', {
     animateOption: {
       update: {
         duration: 1000,
         easing: 'easeLinear'
       }
     }
   });
   setAnnotation(chart, text)
   
   chart
     .interval()
     .position('zone*value')
     .color('zone', colors)
     .label('value', function (value) {
       return {
         animate: {
           appear: {
             animation: 'label-appear',
             delay: 0,
             duration: 1000,
             easing: 'easeLinear'
           },
           update: {
             animation: 'label-update',
             duration: 1000,
             easing: 'easeLinear'
           }
         },
         offset: 5,
       };
     }).animate({
       appear: {
         duration: 1000,
         easing: 'easeLinear'
       },
       update: {
         duration: 1000,
         easing: 'easeLinear'
       }
     });
   countUp.once = true
   chart.render();
 } else {
   chart.annotation().clear(true);
   setAnnotation(chart, text)
   chart.changeData(data);
 }
 ++countUp.count;
 if (countUp.count === Object.keys(dataSource).length) {
   if (config.loop) {
     countUp.count = 0
   } else {
       clearInterval(interval);
       $replay.style.display = 'block'
   }
 }
}
function replay () {
 $replay.style.display = 'none'
 clearInterval(interval)
 countUp.count = 0
 countUp()
 interval = setInterval(countUp, config.interval)
}
window.addEventListener('load', function () {
 $replay.addEventListener('click', replay)
})
window.addEventListener('unload', function() {
 $replay.removeEventListener('click', replay)
});
   
function renderChart (data) {
   if (!data) return
   countUp.dataSource = transformData(data[0])
   replay()
}
new GDPlugin().init(renderChart)

5.2 Highcharts 实现

Highcharts版除了javascript,其他(数据视图的维度和数值定义,CSS代码块)与AntV G2都一致。在config配置上,少了一个padding(因为highcharts能根据标签长度自动调整间距)。

a. HTML代码块

image.png

b. CSS代码块

#container {
   width: 100%;
   height: 100%;
   overflow: auto;
   font-size: 12px;
   color: #343d50;
}
.replay {
   position: fixed;
   top: 10px;
   right: 10px;
}

c. JavaScript代码块

javascript
var config = {
 loop: false, // 是否循环播放 false: 不循环播放 true: 循环播放,(布尔值,无需引号包裹)
 interval: 1200, // 轮播间隔时间 默认为1200ms, 小于1000ms 以1000ms计, (数值,无需引号包裹)
 sort: 'asc', // 图排序方式 asc: 升序  desc: 降序(默认) 以左下角为原点,(字符串,需用英文引号包裹)
 type: 'bar', // bar: 条形图  column: 柱状图,(字符串,需用英文引号包裹)
 showCount: Infinity, // Infinity: 显示全部(默认),为数字时如10,表示图表中最大显示10个分类,(数值,无需引号包裹)
 textStyle: { // 动态维度
   fontSize: 40, // 字体大小,(数值,无需引号包裹)
   fontWeight: 'bold', // 字体粗细 bold: 加粗(默认) normal: 不加粗,(字符串,需用英文引号包裹)
   color: '#ddd', // 字体颜色,(字符串,需用英文引号包裹)
 },
 colors10: [
   '#5B8FF9',
   '#5AD8A6',
   '#5D7092',
   '#F6BD16',
   '#E86452',
   '#6DC8EC',
   '#945FB9',
   '#FF9845',
   '#1E9493',
   '#FF99C3',
 ], // 分类颜色色板,分类个数小于等于 10 时使用
 colors20: [
   '#5B8FF9',
   '#CDDDFD',
   '#5AD8A6',
   '#CDF3E4',
   '#5D7092',
   '#CED4DE',
   '#F6BD16',
   '#FCEBB9',
   '#E86452',
   '#F8D0CB',
   '#6DC8EC',
   '#D3EEF9',
   '#945FB9',
   '#DECFEA',
   '#FF9845',
   '#FFE0C7',
   '#1E9493',
   '#BBDEDE',
   '#FF99C3',
   '#FFE0ED',
 ], // 分类颜色色板,分类个数小于 20 时使用
}
function transformData (data) {
   var result = {}
   var q = data[0]
   var zones = data[1]
   var values = data[2]
   
   q.data.forEach(function (item, index) {
       if (result[item]) {
           result[item].push([ zones.data[index], values.data[index] ])
       } else { result[item] = [[ zones.data[index], values.data[index] ]] }
   })
   return result
}
var sortIsDesc = config.sort === 'desc'
var typeIsBar = config.type === 'bar'
function getPosition (chart, label) {
   var x
   var y
   switch (true) {
       case typeIsBar && !sortIsDesc:
           x = chart.plotWidth + chart.plotLeft - label.width - 10
           y = chart.plotHeight + chart.plotTop - label.height - 10
           break
       case !typeIsBar && !sortIsDesc:
           x = chart.plotLeft + 10
           y = chart.plotTop + 10
           break
       case !typeIsBar && sortIsDesc:
           x = chart.plotWidth + chart.plotLeft - label.width - 10
           y = chart.plotTop + 10
           break
       case typeIsBar && sortIsDesc:
       default:
           x = chart.plotWidth + chart.plotLeft - label.width - 10
           y = chart.plotTop + 10
   }
   return {
       x: x,
       y: y,
   }
}
var chart = Highcharts.chart('container', {
   plotOptions: {
       bar: {
           colorByPoint: true,
       },
       column: {
           colorByPoint: true,
       },
   },
   chart: {
       type: config.type,
       marginLeft: 100,
       
   },
   title: {
       text: '',
   },
   yAxis: {
       title: {
           text: ''
       },
   },
   xAxis: {
       type: 'category',
       labels: {
           animate: true
       },
       reversed: config.sort === 'asc',
   },
   tooltip: {
       enabled: false,
   },
   legend: {
       enabled: false
   },
   series: [{
       dataLabels: {
           enabled: true,
           format: '{y:,.0f}'
       },
       dataSorting: {
           enabled: true,
           sortKey: 'y'
       },
       data: [],
       
   }]
});
var interval
var label
window.addEventListener('load', function () {
 document.querySelector('.replay').addEventListener('click', replay)
})
window.addEventListener('unload', function() {
 document.querySelector('.replay').removeEventListener('click', replay)
});
function replay () {
 document.querySelector('.replay').style.display = 'none'
 clearInterval(interval)
 countUp.count = 0
 countUp()
}
function countUp () {
   var count = countUp.count
   var dataSource = countUp.dataSource
 
   var textData = Object.keys(dataSource)
   
   var seriesData = Object.values(dataSource)
   
   var length = seriesData.length
   if (countUp.label) countUp.label.destroy()
   countUp.label = chart.renderer.label(textData[count], null, null).add()
   countUp.label.css({
       fontSize: `${config.textStyle.fontSize}px`,
       fontWeight: config.textStyle.fontWeight,
       color: config.textStyle.color,
   }).attr(getPosition(chart, countUp.label))
   var colors = seriesData[0].slice(0, config.showCount).length <= 10 ? config.colors10 : config.colors20
   Highcharts.setOptions({ colors: colors })
   chart.series[0].setData(
       seriesData[countUp.count].map(function (item) { return item.slice(0) }).slice(0, config.showCount),
       true,
       { duration: 1000 }
   )
   ++countUp.count
   interval = setInterval(function () {
       countUp.label.attr({
           text: textData[countUp.count]
       })
       chart.series[0].setData(
           seriesData[countUp.count].map(function (item) { return item.slice(0) }).slice(0, config.showCount),
           true,
           { duration: 1000 }
       )
       ++countUp.count
       if (countUp.count === length) {
           if (config.loop) {
               countUp.count = 0
           } else {
               clearInterval(interval)
               document.querySelector('.replay').style.display = 'block'
           }
       }
   }, config.interval < 1000 ? 1000 : config.interval)
}
function renderChart (data) {
   if (!data) return null
   countUp.dataSource = transformData(data[0])
   replay()
}
new GDPlugin().init(renderChart)

5.3 ECharts 实现

Echarts与AntV G2的不同点在于,配置项中暂时没有柱状图形式(type = 'column'),没有padding,且非无限循环时播放按钮一直存在。

a. HTML 代码块

1.png

b. CSS 代码块

#container {
   width: 100%;
   height: 100%;
   overflow: auto;
   font-size: 12px;
   color: #343d50;
}
.replay {
   position: fixed;
   top: 10px;
   right: 10px;
}

c. JavaScript 代码块

var config = {
 loop: false, // 是否循环播放 false: 不循环播放 true: 循环播放,(布尔值,无需引号包裹)
 interval: 1200, // 轮播间隔时间 默认为1200ms, (数值,无需引号包裹)
 sort: 'asc', // 图排序方式 asc: 升序  desc: 降序(默认) 以左下角为原点,(字符串,需用英文引号包裹)
 showCount: Infinity, // Infinity: 显示全部(默认),为数字时如10,表示图表中最大显示10个分类,(数值,无需引号包裹)
 textStyle: { // 动态维度
   fontSize: 40, // 字体大小,(数值,无需引号包裹)
   fontWeight: 'bold', // 字体粗细 bold: 加粗(默认) normal: 不加粗,(字符串,需用英文引号包裹)
   color: '#ddd', // 字体颜色,(字符串,需用英文引号包裹)
 },
 colors10: [
   '#5B8FF9',
   '#5AD8A6',
   '#5D7092',
   '#F6BD16',
   '#E86452',
   '#6DC8EC',
   '#945FB9',
   '#FF9845',
   '#1E9493',
   '#FF99C3',
 ], // 分类颜色色板,分类个数小于等于 10 时使用
 colors20: [
   '#5B8FF9',
   '#CDDDFD',
   '#5AD8A6',
   '#CDF3E4',
   '#5D7092',
   '#CED4DE',
   '#F6BD16',
   '#FCEBB9',
   '#E86452',
   '#F8D0CB',
   '#6DC8EC',
   '#D3EEF9',
   '#945FB9',
   '#DECFEA',
   '#FF9845',
   '#FFE0C7',
   '#1E9493',
   '#BBDEDE',
   '#FF99C3',
   '#FFE0ED',
 ], // 分类颜色色板,分类个数小于 20 时使用
}
var sortIsDesc = config.sort === 'desc'
function transformData (data) {
   var result = {}
   var q = data[0]
   var zones = data[1]
   var values = data[2]
   q.data.forEach(function (item, index) {
       if (result[item]) {
           result[item].push({ value: values.data[index], name: zones.data[index] })
       } else { result[item] = [{ value: values.data[index], name: zones.data[index] }] }
   })
   return result
}
function handleData(source) {
 source.sort(function (a, b) {
   return a.value - b.value
 });
 return source;
}
function getPosition () {
 var left
 var top
 var bottom
 switch (true) {
     case !sortIsDesc:
         right = 10
         bottom = 20
         break
     case sortIsDesc:
     default:
         right = 10
         top = 10
 }
 return {
     right: right,
     top: top,
     bottom: bottom,
 }
}
var position = getPosition()
var $replay = document.querySelector('.replay')
var myChart = echarts.init(document.getElementById('container'))
var option = {
 baseOption: {
   animationDurationUpdate: 1000,
   animationEasingUpdate: 'quinticInOut',
   title: {
     textStyle: config.textStyle,
     right: position.right,
     top: position.top,
     bottom: position.bottom,
   },
   timeline: {
       data: [],
       axisType: 'category',
       autoPlay: true,
       show: false,
       playInterval: config.interval,//播放速度
       loop: config.loop,
   },
   grid: {
     left: 0,
     bottom: 0,
     top: 0,
     right: '5%',
     containLabel: true,
   },
   xAxis: {
     type: 'value'
   },
   yAxis: {
     type: 'category',
     inverse: config.sort === 'desc',
     data: [],
   },
   series: {
       name: '销量',
       type: 'bar',
       label: {      
         show: true,
         position: 'right',
       },
       itemStyle: {
       }
   },
 },
 options: []
};
// 使用刚指定的配置项和数据显示图表。
var $replay = document.querySelector('.replay')
function replay () {
 myChart.setOption(option, true)
}
window.addEventListener('load', function () {
 if (config.loop) {
   $replay.style.display = 'none'
 } else {
   $replay.addEventListener('click', replay)
 }
 
})
window.addEventListener('unload', function() {
 $replay.removeEventListener('click', replay)
});
 
function renderChart (data) {
   if (!data) return
   const dataSource = transformData(data[0])
   
   for (var dynamicVar in dataSource) {
     handleData(dataSource[dynamicVar])
     const yAxisData = dataSource[dynamicVar].map(item => item.name).slice(0, config.showCount)
     option.options.push({
       title: {
         text: dynamicVar
       },
       yAxis: {
         data: yAxisData,
       },
       series: {
         data: dataSource[dynamicVar].slice(0, config.showCount)
       }
     })
     option.baseOption.series.itemStyle.color = function (params) {
       const colors = dataSource[dynamicVar].slice(0, config.showCount).length <= 10 ? config.colors10 : config.colors20
       return colors[yAxisData.indexOf(params.name)]
     }
     option.baseOption.timeline.data.push(dynamicVar)
   }
   myChart.setOption(option);
   
}
new GDPlugin().init(renderChart)