产品文档

【开发文档】自定义图表_BI-124967

插件接口

CustomComponentProvider

package com.finebi.provider.api.component;

import com.finebi.common.context.OperationContext;
import com.finebi.provider.api.component.data.DataModel;
import com.fr.common.annotations.Open;
import com.fr.stable.fun.mark.Mutable;
import com.fr.web.struct.AssembleComponent;
import java.util.List;

@Open
public interface CustomComponentProvider extends Mutable {
    String XML_TAG = "CustomComponentProvider";
    int CURRENT_LEVEL = 1;

    /**
     * 自定义图表名称
     *
     * @return
     */
    String getName();

    /**
     * 自定义图表类型
     *
     * @return
     */
    String getType();

    /**
     * 自定义图表 icon
     *
     * @return
     */
    String getIcon();

    /**
     * 空自定义图表提示,不写默认取 icon
     *
     * @return
     */
    String getPreviewIcon();

    /**
     * 自定义图表编辑 dom,不写默认取预览
     *
     * @param var1 上下文
     * @return
     */
    String getEditPageHTML(OperationContext var1);

    /**
     * 自定义图表渲染 js、css 注入
     *
     * @param var1 上下文
     * @return
     */
    AssembleComponent editClient(OperationContext var1);

    /**
     * 自定义图表预览 dom,注入依赖文件和挂载节点,可以获取 context
     *
     * @param var1 上下文
     * @return
     */
    String getPreviewPageHTML(OperationContext var1);

    /**
     * 自定义图表渲染 js、css 注入
     *
     * @param var1 上下文
     * @return
     */
    AssembleComponent previewClient(OperationContext var1);

    /**
     * 自定义图表配置文件
     *
     * @return 配置文件,json字符串
     */
    String config();

    /**
     * 返回是否需要进行自定义数据处理
     *
     * @param var1 自定义图表相关的上下文信息,目前包含前端传的查询配置
     * @return 是否需要进行自定义数据处理
     */
    boolean needDataProcess(CustomComponentContext var1);

    /**
     * 对BI计算后即将返回前端的数据进行自定义处理
     *
     * @param var1 数据模型
     * @param var2 自定义图表相关的上下文信息,目前包含前端传的查询配置
     * @return 处理过的数据模型
     */
    List<DataModel> process(List<DataModel> var1, CustomComponentContext var2);
}

AbstractCustomComponentProvider

package com.finebi.provider.api.component;

import com.finebi.common.context.OperationContext;
import com.finebi.provider.api.component.data.DataModel;
import com.fr.stable.fun.mark.API;
import com.fr.web.struct.AssembleComponent;
import java.util.List;

@API(
    level = 1
)
public abstract class AbstractCustomComponentProvider implements CustomComponentProvider {
    public AbstractCustomComponentProvider() {
    }

    public String getPreviewIcon() {
        return this.getIcon();
    }

    public String getEditPageHTML(OperationContext context) {
        return this.getPreviewPageHTML(context);
    }

    public AssembleComponent editClient(OperationContext context) {
        return this.previewClient(context);
    }

    public int currentAPILevel() {
        return 1;
    }

    public String mark4Provider() {
        return this.getClass().getName();
    }

    public boolean needDataProcess(CustomComponentContext customComponentContext) {
        return false;
    }

    public List<DataModel> process(List<DataModel> dataModels, CustomComponentContext customComponentContext) {
        return dataModels;
    }
}

基础教程

通过上述接口,开发高德地图图表

一、向前端图表类型添加图表选项

继承AbstractCustomComponentProvider接口,实现相应功能

import com.finebi.common.context.OperationContext;
import com.finebi.plugin.tptj.ivan.chart.demo.amap.component.MapHotComponent;
import com.finebi.plugin.tptj.ivan.chart.demo.amap.constant.PluginConstantsEK;
import com.finebi.provider.api.component.AbstractCustomComponentProvider;
import com.fr.base.TemplateUtils;
import com.fr.general.IOUtils;
import com.fr.intelli.record.Focus;
import com.fr.intelli.record.Original;
import com.fr.record.analyzer.EnableMetrics;
import com.fr.web.struct.AssembleComponent;

@EnableMetrics
public class MapHotComponentProvider extends AbstractCustomComponentProvider {
    /**
     * 图表名称
     */
    @Override
    public String getName() {
        return PluginConstantsEK.PLUGIN_MAP_NAME;
    }

    /**
     * @return 图表类型
     */
    @Override
    public String getType() {
        return PluginConstantsEK.PLUGIN_MAP_TYPE;
    }

    /**
     * 图标
     *
     * @return icon的地址
     */
    @Override
    public String getIcon() {
        try {
            String render = TemplateUtils.render("${fineServletURL}");
            return render + "/resources?path=/com/finebi/plugin/tptj/ivan/chart/demo/amap/icon.png";
        } catch (Exception ignore) {
        }
        return "";
    }

    /**
     * @param context 上下文
     * @return 初始化时的页面元素
     */
    @Focus(id = PluginConstantsEK.PLUGIN_ID, text = PluginConstantsEK.PLUGIN_NAME, source = Original.PLUGIN)
    @Override
    public String getPreviewPageHTML(OperationContext context) {
        return "<div id=\"amap-demo-container\"></div>";
    }

    @Override
    public AssembleComponent previewClient(OperationContext context) {
        return MapHotComponent.KEY;
    }

    /**
     * 配置项,JSON字符串格式
     *
     * @return
     */
    @Override
    public String config() {
        return IOUtils.readResourceAsString("com/finebi/plugin/tptj/ivan/chart/demo/amap/config/config.json");
    }
}

plugin.xml

<extra-core>
    <CustomComponentProvider
            class="com.finebi.plugin.tptj.ivan.chart.demo.amap.MapHotComponentProvider"/>
</extra-core> 

效果图

二、向前端图表类型添加对应的配置

实现AbstractCustomComponentProvider接口的config方法,返回值为json格式字符串

{
  "dataRegions": [
    {
      "name": "lat",
      "text": "Plugin-DEMO_WEB_LAT"
    },
    {
      "name": "lng",
      "text": "Plugin-DEMO_WEB_LNG"
    }
  ],
  "attrRegions": [
    {
      "name": "细粒度",
      "text": "Plugin-DEMO_WEB_FG",
      "multiFields": false,
      "settings": []
    }
  ],
  "chartStyles": [
    {
      "name": "mapProp",
      "text": "Plugin-DEMO_WEB_MAP_ATTRIBUTE",
      "multiFields": true,
      "settings": [
        {
          "name": "centerLng",
          "text": "Plugin-DEMO_WEB_CENTER_LNG",
          "type": "Input",
          "defaultValue": "102.3716"
        },
        {
          "name": "centerLat",
          "text": "Plugin-DEMO_WEB_CENTER_LAT",
          "type": "Input",
          "defaultValue": "36.6808"
        },
        {
          "name": "defaultZoom",
          "text": "Plugin-DEMO_WEB_DEFAULT_ZOOM",
          "type": "Input",
          "defaultValue": "4"
        },
        {
          "name": "style",
          "text": "Plugin-DEMO_WEB_STYLE",
          "type": "Select",
          "defaultValue": "normal",
          "items": [
            {
              "text": "标准",
              "value": "normal"
            },
            {
              "text": "幻影黑",
              "value": "dark"
            },
            {
              "text": "月光银",
              "value": "light"
            },
            {
              "text": "远山黛",
              "value": "whitesmoke"
            },
            {
              "text": "草色青",
              "value": "fresh"
            },
            {
              "text": "雅士灰",
              "value": "grey"
            },
            {
              "text": "涂鸦",
              "value": "graffiti"
            },
            {
              "text": "马卡龙",
              "value": "macaron"
            },
            {
              "text": "靛青蓝",
              "value": "blue"
            },
            {
              "text": "极夜蓝",
              "value": "darkblue"
            },
            {
              "text": "酱籽",
              "value": "wine"
            }
          ]
        }
      ]
    }
  ]
}

效果图

JSON配置文件说明

注:配置项不支持自定义格式、不支持配置项联动、仅支持以上几种基础格式,按需选择。

配置支持国际化,name代表id,前端js需要通过name获取相应的值,text代表显示名称,可以传入国际化key

{
    "dataRegions": [
        {
            "name": "自定义数据1",
			"text": "国际化key"
        },
        {
            "name": "自定义数据2"
        }
    ],
    "attrRegions": [
        {
            "name": "颜色1",
            "multiFields": false,
            "settings": "color"
        },
        {
            "name": "大小1",
            "multiFields": true,
            "settings": "size"
        },
        {
            "name": "形状1",
            "multiFields": false,
            "settings": "symbol"
        },
        {
            "name": "自定选项",
            "multiFields": false,
            "settings": [
                {
                    "name": "Checkbox",
                    "type": "Checkbox",
                    "defaultValue": ["2", "3"],
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        },
                        {
                            "text": "选项2",
                            "value": "3"
                        }
                    ]
                },
                {
                    "name": "RadioGroup",
                    "type": "RadioGroup",
                    "defaultValue": "2",
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        }
                    ]
                },
                {
                    "name": "Segment",
                    "type": "Segment",
                    "defaultValue": "2",
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        }
                    ]
                },
                {
                    "name": "Select",
                    "type": "Select",
                    "defaultValue": "1",
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        }
                    ]
                },
                {
                    "name": "ColorPicker",
                    "type": "ColorPicker",
                    "defaultValue": "#ffffff"
                },
                {
                    "name": "Input",
                    "type": "Input",
                    "defaultValue": "test"
                }
            ]
        },
        {
            "name": "属性1属性1属性1",
            "multiFields": true
        },
        {
            "name": "属性2",
            "multiFields": false
        }
    ],
    "chartStyles": [
        {
            "name": "自定选项",
            "settings": [
                {
                    "name": "Checkbox",
                    "type": "Checkbox",
                    "defaultValue": ["2", "3"],
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        },
                        {
                            "text": "选项2",
                            "value": "3"
                        }
                    ]
                },
                {
                    "name": "RadioGroup",
                    "type": "RadioGroup",
                    "defaultValue": "2",
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        }
                    ]
                },
                {
                    "name": "Segment",
                    "type": "Segment",
                    "defaultValue": "2",
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        }
                    ]
                },
                {
                    "name": "Select",
                    "type": "Select",
                    "defaultValue": "1",
                    "items": [
                        {
                            "text": "选项1",
                            "value": "1"
                        },
                        {
                            "text": "选项2",
                            "value": "2"
                        }
                    ]
                },
                {
                    "name": "ColorPicker",
                    "type": "ColorPicker",
                    "defaultValue": "#ffffff"
                },
                {
                    "name": "Input",
                    "type": "Input",
                    "defaultValue": "test"
                }
            ]
        }
    ]
}

三、将图表显示到前端页面上

前端接口

function render(
	data, // 数据
	config, // 配置
	saveSessionCallback,
	closeSessionCallBack,
	extensionCallBack
) {}

// 注册渲染方法
new BIPlugin().init(render);

具体实现

(function ($) {
  function render(
    data, // 数据
    config, // 配置
    saveSessionCallback,
    closeSessionCallBack,
    extensionCallBack
  ) {
    // 查找初始化时的dom对象,该对象在 AbstractCustomComponentProvider#getPreviewPageHTML 方法中定义
    const dom = document.getElementById("amap-demo-container");

    // 一定要设置 宽高   不然图表渲染不出来
    dom.style.width = document.body.clientWidth + "px";
    dom.style.height = document.body.clientHeight + "px";

    // 获取配置项
    const mapAttribute = config["chartStyle"]["地图属性"]["value"];

    // 地图组件初始化
    const map = new AMap.Map(dom, {
      resizeEnable: true,
      center: [mapAttribute[0], mapAttribute[1]],
      zoom: mapAttribute[2],
    });

    // 地图设置样式
    const styleName = "amap://styles/" + mapAttribute[3];
    map.setMapStyle(styleName);

    window.addEventListener("resize", function () {
      dom.style.width = document.body.clientWidth + "px";
      dom.style.height = document.body.clientHeight + "px";
    });
  }

  // 注册渲染方法
  new BIPlugin().init(render);
})(jQuery);

效果图

四、前端读取数据

上述js接口中,data代表其数据维度和指标,config内包含了图形属性和组件样式

(function ($) {
    function render(
        data, // 数据
        config, // 配置
        saveSessionCallback,
        closeSessionCallBack,
        extensionCallBack
    ) {
        debugger;

        // 查找初始化时的dom对象
        const dom = document.getElementById("amap-demo-container");

        // 一定要设置 宽高   不然图表渲染不出来
        dom.style.width = document.body.clientWidth + "px";
        dom.style.height = document.body.clientHeight + "px";

        // 获取配置项
        const mapAttribute = config["chartStyle"]["地图属性"]["value"];

        // 地图组件初始化
        const map = new AMap.Map(dom, {
            resizeEnable: true,
            center: [mapAttribute[0], mapAttribute[1]],
            zoom: mapAttribute[2],
        });

        // 地图设置样式
        const styleName = "amap://styles/" + mapAttribute[3];
        map.setMapStyle(styleName);

        // 获取全部的经纬度点数据
        const points = _getAllPoint(data.dataMapping, data.dataModels[0]);

        if (points != null) {
            // 创建聚合点图层
            var pointCluster = new AMap.MarkerCluster(map, points, {
                gridSize: 60, // 设置网格像素大小
                renderClusterMarker: _renderCarClusterMarker, // 自定义聚合点样式
                renderMarker: _renderMarker,   // 自定义非聚合点样式
            });
        }


        window.addEventListener("resize", function () {
            dom.style.width = document.body.clientWidth + "px";
            dom.style.height = document.body.clientHeight + "px";
        });
    }

    /**
     * 自定义聚合点样式
     * @param context
     * @private
     */
    function _renderCarClusterMarker(context) {
        let count = context.count;
        let factor = context.count / count;
        const div = document.createElement('div');
        const Hue = 180 - factor * 180;
        let bgColor;
        if (context.count < 10) {
            bgColor = 'hsla(108,100%,40%,1)';
        } else if (context.count < 100) {
            bgColor = 'hsl(201,100%,40%)';
        } else if (context.count < 1000) {
            bgColor = 'hsla(36,100%,50%,1)';
        } else if (context.count < 10000) {
            bgColor = 'hsla(0,100%,60%,1)';
        } else {
            bgColor = 'hsla(0,100%,40%,1)';
        }
        const fontColor = 'hsla(' + Hue + ',100%,90%,1)';
        const borderColor = bgColor;
        const shadowColor = 'hsla(' + Hue + ',100%,90%,1)';
        div.style.backgroundColor = bgColor;
        const size = Math.round(30 + Math.pow(context.count / count, 1 / 5) * 20);
        div.style.width = div.style.height = size + 'px';
        div.style.border = 'solid 1px ' + borderColor;
        div.style.borderRadius = size / 2 + 'px';
        div.style.boxShadow = '0 0 5px ' + shadowColor;
        div.innerHTML = context.count;
        div.style.lineHeight = size + 'px';
        div.style.color = fontColor;
        div.style.fontSize = '14px';
        div.style.textAlign = 'center';
        context.marker.setOffset(new AMap.Pixel(-size / 2, -size / 2));
        context.marker.setContent(div)
    }

    /**
     * 自定义非聚合点样式
     * @param context
     * @private
     */
    function _renderMarker(context) {
        var content = '<div style="background-color: rgba(255,255,178,.9); height: 18px; width: 18px; border: 1px solid rgba(255,255,178,1); border-radius: 12px; box-shadow: rgba(0, 0, 0, 1) 0px 0px 3px;"></div>';
        var offset = new AMap.Pixel(-9, -9);
        context.marker.setContent(content)
        context.marker.setOffset(offset)
    }

    /**
     *
     * @param dataMapping
     * @param dataModel
     * @returns {*[]}
     * @private
     */
    function _getAllPoint(dataMapping, dataModel) {
        let lngId = dataMapping['经度'];
        let latId = dataMapping['纬度'];

        let files = dataModel.fields;
        let rowCount = dataModel.rowCount
        const colData = dataModel.colData;

        let lngIndex;
        let latIndex;

        // demo这里没有考虑多维度多指标的情况,具体根据实际情况进行修改
        files.forEach((item, index) => {
            if (lngId.indexOf(item.id) >= 0) {
                lngIndex = index;
            }
            if (latId.indexOf(item.id) >= 0) {
                latIndex = index;
            }
        })

        var points = [];
        for (let i = 0; i < rowCount; i++) {
            points.push({
                lnglat: [colData[lngIndex][i], colData[latIndex][i]],
            })
        }

        return points;
    }

    // 注册渲染方法
    new BIPlugin().init(render);
})(jQuery);

注:接口无法直接获取明细数据,若希望使用明细数据,则可将其转为维度,或添加细粒度属性。

效果图

通过细粒度进行划分

通过维度进行划分

DEMO

上述效果图DEMO源码

进阶教程

一、数据处理接口

一般情况下产品本身的处理方式已经够用了,就没必要使用该接口了

场景:产品已有的数据处理方式无法实现客户的需求,希望在数据返回到前端前进行额外的处理

注意:此时插件逻辑中能拿到的数据是产品已经处理过的,可以理解为插件处理在产品处理后。甚至可以替换掉原本的dataModels。具体的处理方式需要自己实现,但要注意性能问题

实现方式

重写CustomComponentProvider接口中的needDataProcess和process方法

/**
    * 是否需要进行数据处理
    * true:需要
    * false:不需要
    * CustomComponentContext: 自定义图表相关配置
    */
@Override
public boolean needDataProcess(CustomComponentContext customComponentContext) {
    return true;
}

/**
    * 数据处理逻辑
    * needDataProcess返回true时才会生效
    * CustomComponentContext: 自定义图表相关配置
    */
@Override
public List<DataModel> process(List<DataModel> dataModels, CustomComponentContext customComponentContext) {
    return dataModels;
}

示例

期望效果:上述图表DEMO中,每次只随机返回一个点的数据

/**
    * 是否需要进行数据处理
    * true:需要
    * false:不需要
    * CustomComponentContext: 自定义图表相关配置
    */
@Override
public boolean needDataProcess(CustomComponentContext customComponentContext) {
    return true;
}

/**
    * 数据处理逻辑
    * needDataProcess返回true时才会生效
    * CustomComponentContext: 自定义图表相关配置
    */
@Override
public List<DataModel> process(List<DataModel> dataModels, CustomComponentContext customComponentContext) {
    return dataModels.stream().map(dataModel -> new DataModel() {
        @Override
        public List<Dimension> getFields() {
            return dataModel.getFields();
        }

        @Override
        public List<List<Object>> getColData() {
            List<List<Object>> colData = new ArrayList<>(dataModel.getFields().size());
            dataModel.getColData().forEach(d -> colData.add(Collections.singletonList(d.get((int) (Math.random() * d.size())))));
            return colData;
        }
    }).collect(Collectors.toList());
}

效果图

DEMO

上述效果图DEMO源码

二、页面刷新接口

场景:需要客户手动去重新刷新图表,而非整个页面

实现方式

前端JS调用extensionCallBack('refresh')函数。extensionCallBack即为render方法中的第三个参数

function render(data, config, saveSessionCallback, closeSessionCallBack, extensionCallBack) {
    function userClick(){
        // 每次执行该方法都会刷新iframe,因此调用前需有逻辑判断。
        extensionCallBack('refresh');
    }
}

// 注册渲染方法
new BIPlugin().init(render);

示例

期望效果:上述图表DEMO中,每次点击数据点时刷新该图表iframe

(function ($) {
    function render(
        data, // 数据
        config, // 配置
        saveSessionCallback,
        closeSessionCallBack,
        extensionCallBack
    ) {
        debugger;

        // 查找初始化时的dom对象
        const dom = document.getElementById("amap-demo-container");

        // 一定要设置 宽高   不然图表渲染不出来
        dom.style.width = document.body.clientWidth + "px";
        dom.style.height = document.body.clientHeight + "px";

        // 获取配置项
        const mapAttribute = config["chartStyle"]["mapProp"]["value"];

        // 地图组件初始化
        const map = new AMap.Map(dom, {
            resizeEnable: true,
            center: [mapAttribute[0], mapAttribute[1]],
            zoom: mapAttribute[2],
        });

        // 地图设置样式
        const styleName = "amap://styles/" + mapAttribute[3];
        map.setMapStyle(styleName);

        // 获取全部的经纬度点数据
        const points = _getAllPoint(data.dataMapping, data.dataModels[0]);

        if (points != null) {
            // 创建聚合点图层
            var pointCluster = new AMap.MarkerCluster(map, points, {
                gridSize: 60, // 设置网格像素大小
                renderClusterMarker: _renderCarClusterMarker, // 自定义聚合点样式
                renderMarker: _renderMarker,   // 自定义非聚合点样式
            });
        }


        window.addEventListener("resize", function () {
            dom.style.width = document.body.clientWidth + "px";
            dom.style.height = document.body.clientHeight + "px";
        });

        document.querySelector("#amap-demo-click").onclick = function () {
            extensionCallBack('refresh');
        }
    }

    /**
     * 自定义聚合点样式
     * @param context
     * @private
     */
    function _renderCarClusterMarker(context) {
        let count = context.count;
        let factor = context.count / count;
        const div = document.createElement('div');
        const Hue = 180 - factor * 180;
        let bgColor;
        if (context.count < 10) {
            bgColor = 'hsla(108,100%,40%,1)';
        } else if (context.count < 100) {
            bgColor = 'hsl(201,100%,40%)';
        } else if (context.count < 1000) {
            bgColor = 'hsla(36,100%,50%,1)';
        } else if (context.count < 10000) {
            bgColor = 'hsla(0,100%,60%,1)';
        } else {
            bgColor = 'hsla(0,100%,40%,1)';
        }
        const fontColor = 'hsla(' + Hue + ',100%,90%,1)';
        const borderColor = bgColor;
        const shadowColor = 'hsla(' + Hue + ',100%,90%,1)';
        div.style.backgroundColor = bgColor;
        const size = Math.round(30 + Math.pow(context.count / count, 1 / 5) * 20);
        div.style.width = div.style.height = size + 'px';
        div.style.border = 'solid 1px ' + borderColor;
        div.style.borderRadius = size / 2 + 'px';
        div.style.boxShadow = '0 0 5px ' + shadowColor;
        div.innerHTML = context.count;
        div.style.lineHeight = size + 'px';
        div.style.color = fontColor;
        div.style.fontSize = '14px';
        div.style.textAlign = 'center';
        context.marker.setOffset(new AMap.Pixel(-size / 2, -size / 2));
        context.marker.setContent(div)
    }

    /**
     * 自定义非聚合点样式
     * @param context
     * @private
     */
    function _renderMarker(context) {
        var content = '<div style="background-color: rgba(255,255,178,.9); height: 18px; width: 18px; border: 1px solid rgba(255,255,178,1); border-radius: 12px; box-shadow: rgba(0, 0, 0, 1) 0px 0px 3px;"></div>';
        var offset = new AMap.Pixel(-9, -9);
        context.marker.setContent(content)
        context.marker.setOffset(offset)
    }

    /**
     *
     * @param dataMapping
     * @param dataModel
     * @returns {*[]}
     * @private
     */
    function _getAllPoint(dataMapping, dataModel) {
        let lngId = dataMapping['lat'];
        let latId = dataMapping['lng'];

        let files = dataModel.fields;
        let rowCount = dataModel.rowCount
        const colData = dataModel.colData;

        let lngIndex;
        let latIndex;

        // demo这里没有考虑多维度多指标的情况,具体根据实际情况进行修改
        files.forEach((item, index) => {
            if (lngId.indexOf(item.id) >= 0) {
                lngIndex = index;
            }
            if (latId.indexOf(item.id) >= 0) {
                latIndex = index;
            }
        })

        var points = [];
        for (let i = 0; i < rowCount; i++) {
            points.push({
                lnglat: [colData[lngIndex][i], colData[latIndex][i]],
            })
        }

        return points;
    }

    // 注册渲染方法
    new BIPlugin().init(render);
})(jQuery);

效果图

DEMO

上述效果图DEMO源码

三、保存配置接口

场景:举个例子,比如地图类需求,用户希望在调用上述刷新接口后,中心点、缩放等级等相关信息,仍和点击刷新前的一致

实现方式

function render(data, config, saveSessionCallback, closeSessionCallBack, extensionCallBack) {
    function userClick(){
		// 这里保存的配置{xxx: yyy},将会保存在config.customConfig中。因此只要config.customConfig有数据,读取即可
	 	saveSessionCallback({xxx: yyy});
        // 每次执行该方法都会刷新iframe,因此调用前需有逻辑判断。
        extensionCallBack('refresh');
    }
}

// 注册渲染方法
new BIPlugin().init(render);

示例

期望效果:上述图表DEMO中,每次点击数据点时刷新该图表iframe后,中心点和缩放等级保持不变

(function ($) {
    function render(
        data, // 数据
        config, // 配置
        saveSessionCallback,
        closeSessionCallBack,
        extensionCallBack
    ) {
        debugger;

        // 查找初始化时的dom对象
        const dom = document.getElementById("amap-demo-container");

        // 一定要设置 宽高   不然图表渲染不出来
        dom.style.width = document.body.clientWidth + "px";
        dom.style.height = document.body.clientHeight + "px";

        // 获取配置项
        const mapAttribute = config["chartStyle"]["mapProp"]["value"];

        // 地图组件初始化
        let map;
        let customConfig = config.customConfig;
        if (customConfig != null && JSON.stringify(customConfig).length > 2) {
            // 读取保存的配置
            map = new AMap.Map(dom, {
                resizeEnable: true,
                center: [customConfig.lng, customConfig.lat],
                zoom: customConfig.zoom,
            });
        } else {
            map = new AMap.Map(dom, {
                resizeEnable: true,
                center: [mapAttribute[0], mapAttribute[1]],
                zoom: mapAttribute[2],
            });
        }

        // 地图设置样式
        const styleName = "amap://styles/" + mapAttribute[3];
        map.setMapStyle(styleName);

        // 获取全部的经纬度点数据
        const points = _getAllPoint(data.dataMapping, data.dataModels[0]);

        if (points != null) {
            // 创建聚合点图层
            var pointCluster = new AMap.MarkerCluster(map, points, {
                gridSize: 60, // 设置网格像素大小
                renderClusterMarker: _renderCarClusterMarker, // 自定义聚合点样式
                renderMarker: _renderMarker,   // 自定义非聚合点样式
            });
        }


        window.addEventListener("resize", function () {
            dom.style.width = document.body.clientWidth + "px";
            dom.style.height = document.body.clientHeight + "px";
        });

        document.querySelector("#amap-demo-click").onclick = function () {
            let conf = {
                zoom: map.getZoom(),
                lng: map.getCenter().lng,
                lat: map.getCenter().lat,
            };
            // 保存配置
            saveSessionCallback(conf);
            extensionCallBack('refresh');
        }
    }

    /**
     * 自定义聚合点样式
     * @param context
     * @private
     */
    function _renderCarClusterMarker(context) {
        let count = context.count;
        let factor = context.count / count;
        const div = document.createElement('div');
        const Hue = 180 - factor * 180;
        let bgColor;
        if (context.count < 10) {
            bgColor = 'hsla(108,100%,40%,1)';
        } else if (context.count < 100) {
            bgColor = 'hsl(201,100%,40%)';
        } else if (context.count < 1000) {
            bgColor = 'hsla(36,100%,50%,1)';
        } else if (context.count < 10000) {
            bgColor = 'hsla(0,100%,60%,1)';
        } else {
            bgColor = 'hsla(0,100%,40%,1)';
        }
        const fontColor = 'hsla(' + Hue + ',100%,90%,1)';
        const borderColor = bgColor;
        const shadowColor = 'hsla(' + Hue + ',100%,90%,1)';
        div.style.backgroundColor = bgColor;
        const size = Math.round(30 + Math.pow(context.count / count, 1 / 5) * 20);
        div.style.width = div.style.height = size + 'px';
        div.style.border = 'solid 1px ' + borderColor;
        div.style.borderRadius = size / 2 + 'px';
        div.style.boxShadow = '0 0 5px ' + shadowColor;
        div.innerHTML = context.count;
        div.style.lineHeight = size + 'px';
        div.style.color = fontColor;
        div.style.fontSize = '14px';
        div.style.textAlign = 'center';
        context.marker.setOffset(new AMap.Pixel(-size / 2, -size / 2));
        context.marker.setContent(div)
    }

    /**
     * 自定义非聚合点样式
     * @param context
     * @private
     */
    function _renderMarker(context) {
        var content = '<div style="background-color: rgba(255,255,178,.9); height: 18px; width: 18px; border: 1px solid rgba(255,255,178,1); border-radius: 12px; box-shadow: rgba(0, 0, 0, 1) 0px 0px 3px;"></div>';
        var offset = new AMap.Pixel(-9, -9);
        context.marker.setContent(content)
        context.marker.setOffset(offset)
    }

    /**
     *
     * @param dataMapping
     * @param dataModel
     * @returns {*[]}
     * @private
     */
    function _getAllPoint(dataMapping, dataModel) {
        let lngId = dataMapping['lat'];
        let latId = dataMapping['lng'];

        let files = dataModel.fields;
        let rowCount = dataModel.rowCount
        const colData = dataModel.colData;

        let lngIndex;
        let latIndex;

        // demo这里没有考虑多维度多指标的情况,具体根据实际情况进行修改
        files.forEach((item, index) => {
            if (lngId.indexOf(item.id) >= 0) {
                lngIndex = index;
            }
            if (latId.indexOf(item.id) >= 0) {
                latIndex = index;
            }
        })

        var points = [];
        for (let i = 0; i < rowCount; i++) {
            points.push({
                lnglat: [colData[lngIndex][i], colData[latIndex][i]],
            })
        }

        return points;
    }

    // 注册渲染方法
    new BIPlugin().init(render);
})(jQuery);

效果图

DEMO

上述效果图DEMO源码

四、组件联动(click仅支持数据联动 已废弃,不再使用)

场景:点击时联动仪表板内的其他组件

实现方式

const currentClicked = {
    dId: id,
    value: val
};
extensionCallBack(
    'click',
    currentClicked
);

示例

期望效果:上述图表DEMO中,每次点击非聚合点后,对其他组件数据进行联动

(function ($) {
    function render(
        data, // 数据
        config, // 配置
        saveSessionCallback,
        closeSessionCallBack,
        extensionCallBack
    ) {
        debugger;

        // 查找初始化时的dom对象
        const dom = document.getElementById("amap-demo-container");

        // 一定要设置 宽高   不然图表渲染不出来
        dom.style.width = document.body.clientWidth + "px";
        dom.style.height = document.body.clientHeight + "px";

        const {
            // 数据,若横纵轴多指标时图表属性有维度字段,dataModels 为以图表属性为分组的多组数据
            dataModels,
            // 横纵轴拖入字段的维度 id
            dataMapping,
            // 图表属性拖入字段的维度 id
            chartAttrMapping,
            // 图表点击值
            clicked
        } = data;

        const {
            // 组件 id
            widgetId,
            // 全局样式中图表最终样式
            globalStyles,
            // 图表属性配置所选值
            chartAttr,
            // 图表样式配置所选值
            chartStyle,
            // 图标属性中使用形状时的图标映射
            symbolIconMap,
            // 自定义保存的值(saveSessionCallback)
            customConfig
        } = config;

        // 获取配置项
        const mapAttribute = chartStyle["mapProp"]["value"];

        // 地图组件初始化
        let map;
        if (customConfig != null && JSON.stringify(customConfig).length > 2) {
            // 读取保存的配置
            map = new AMap.Map(dom, {
                resizeEnable: true,
                center: [customConfig.lng, customConfig.lat],
                zoom: customConfig.zoom,
            });
        } else {
            map = new AMap.Map(dom, {
                resizeEnable: true,
                center: [mapAttribute[0], mapAttribute[1]],
                zoom: mapAttribute[2],
            });
        }

        // 地图设置样式
        const styleName = "amap://styles/" + mapAttribute[3];
        map.setMapStyle(styleName);

        // 获取全部的经纬度点数据
        const points = _getAllPoint(dataMapping, dataModels[0]);

        if (points != null) {
            // 创建聚合点图层
            var pointCluster = new AMap.MarkerCluster(map, points, {
                gridSize: 60, // 设置网格像素大小
                renderClusterMarker: _renderCarClusterMarker, // 自定义聚合点样式
                renderMarker: _renderMarker,   // 自定义非聚合点样式
            });
        }


        window.addEventListener("resize", function () {
            dom.style.width = document.body.clientWidth + "px";
            dom.style.height = document.body.clientHeight + "px";
        });

        document.querySelector("#amap-demo-click").onclick = function () {
            let conf = {
                zoom: map.getZoom(),
                lng: map.getCenter().lng,
                lat: map.getCenter().lat,
            };
            // 保存配置
            saveSessionCallback(conf);
            extensionCallBack('refresh');
        }

        /**
         * 自定义聚合点样式
         * @param context
         * @private
         */
        function _renderCarClusterMarker(context) {
            let count = context.count;
            let factor = context.count / count;
            const div = document.createElement('div');
            const Hue = 180 - factor * 180;
            let bgColor;
            if (context.count < 10) {
                bgColor = 'hsla(108,100%,40%,1)';
            } else if (context.count < 100) {
                bgColor = 'hsl(201,100%,40%)';
            } else if (context.count < 1000) {
                bgColor = 'hsla(36,100%,50%,1)';
            } else if (context.count < 10000) {
                bgColor = 'hsla(0,100%,60%,1)';
            } else {
                bgColor = 'hsla(0,100%,40%,1)';
            }
            const fontColor = 'hsla(' + Hue + ',100%,90%,1)';
            const borderColor = bgColor;
            const shadowColor = 'hsla(' + Hue + ',100%,90%,1)';
            div.style.backgroundColor = bgColor;
            const size = Math.round(30 + Math.pow(context.count / count, 1 / 5) * 20);
            div.style.width = div.style.height = size + 'px';
            div.style.border = 'solid 1px ' + borderColor;
            div.style.borderRadius = size / 2 + 'px';
            div.style.boxShadow = '0 0 5px ' + shadowColor;
            div.innerHTML = context.count;
            div.style.lineHeight = size + 'px';
            div.style.color = fontColor;
            div.style.fontSize = '14px';
            div.style.textAlign = 'center';
            context.marker.setOffset(new AMap.Pixel(-size / 2, -size / 2));
            context.marker.setContent(div)
        }

        /**
         * 自定义非聚合点样式
         * @param context
         * @private
         */
        function _renderMarker(context) {
            var content = '<div style="background-color: rgba(255,255,178,.9); height: 18px; width: 18px; border: 1px solid rgba(255,255,178,1); border-radius: 12px; box-shadow: rgba(0, 0, 0, 1) 0px 0px 3px;"></div>';
            var offset = new AMap.Pixel(-9, -9);
            context.marker.setContent(content)
            context.marker.setOffset(offset)
            let datum = context.data[0];
            let id = datum.idId;
            const val = [
                {
                    dId: datum.idId,
                    text: datum.idVal,
                },
                // {
                //     dId: datum.lngId,
                //     text: datum.lngVal,
                // },
                // {
                //     dId: datum.latId,
                //     text: datum.latVal,
                // }
            ];

            /**
             * dId: 当前维度的Id
             * value: 当前维度和之前维度的id及对应值  (不是全部的维度信息!)
             */
            const currentClicked = {
                dId: id,
                value: val
            };

            context.marker.on('click', ev => {
                debugger;
                // 触发联动
                extensionCallBack(
                    'click',
                    currentClicked
                );
            })
        }

    }

    /**
     *
     * @param dataMapping
     * @param dataModel
     * @returns {*[]}
     * @private
     */
    function _getAllPoint(dataMapping, dataModel) {
        let lngId = dataMapping['lng'];
        let latId = dataMapping['lat'];

        let files = dataModel.fields;
        let rowCount = dataModel.rowCount
        const colData = dataModel.colData;

        let lngIndex;
        let latIndex;
        let idIndex;

        // demo这里没有考虑多维度多指标的情况,具体根据实际情况进行修改
        files.forEach((item, index) => {
            if (lngId.indexOf(item.id) >= 0) {
                lngIndex = index;
            } else if (latId.indexOf(item.id) >= 0) {
                latIndex = index;
            } else {
                idIndex = index;
            }
        })

        var points = [];
        for (let i = 0; i < rowCount; i++) {
            points.push({
                lnglat: [colData[lngIndex][i], colData[latIndex][i]],
                lngId: files[lngIndex].id,
                latId: files[latIndex].id,
                idId: files[idIndex].id,
                lngVal: colData[lngIndex][i],
                latVal: colData[latIndex][i],
                idVal: colData[idIndex][i],
            })
        }

        return points;
    }

    // 注册渲染方法
    new BIPlugin().init(render);
})(jQuery);

效果图

DEMO

上述效果图DEMO源码

五、组件联动(dimensionSelected,支持跳转和联动)

场景:点击时联动其他组件,或进行跳转

实现方式

请根据用户的数据类型,选择合适的事件

const currentClicked = {
    id1: value1,
    id2: value2,
    id3: value3
};

// 点击指标事件
extensionCallBack("pointSelected", {
    pos: {
        x: 鼠标位置x,
        y: 鼠标位置y,
    },
    // 点击指标的维度id
    measure: measureId,
    // 点击指标所在行的各个字段值,包括指标和维度
    // id: 字段值
    row: currentClicked,
});

// 点击维度事件
extensionCallBack("dimensionSelected", {
    pos: {
        x: 鼠标位置x,
        y: 鼠标位置y,
    },
    // 点击指标的维度id
    measure: measureId,
    // 点击指标所在行的各个字段值,包括指标和维度
    // id: 字段值
    row: currentClicked,
});

示例

期望效果:上述图表DEMO中,每次点击非聚合数据后,可以联动或跳转

(function ($) {
    function render(
        data, // 数据
        config, // 配置
        saveSessionCallback,
        closeSessionCallBack,
        extensionCallBack
    ) {
        debugger;

        // 查找初始化时的dom对象
        const dom = document.getElementById("amap-demo-container");

        // 一定要设置 宽高   不然图表渲染不出来
        dom.style.width = document.body.clientWidth + "px";
        dom.style.height = document.body.clientHeight + "px";

        const {
            // 数据,若横纵轴多指标时图表属性有维度字段,dataModels 为以图表属性为分组的多组数据
            dataModels,
            // 横纵轴拖入字段的维度 id
            dataMapping,
            // 图表属性拖入字段的维度 id
            chartAttrMapping,
            // 图表点击值
            clicked
        } = data;

        const {
            // 组件 id
            widgetId,
            // 全局样式中图表最终样式
            globalStyles,
            // 图表属性配置所选值
            chartAttr,
            // 图表样式配置所选值
            chartStyle,
            // 图标属性中使用形状时的图标映射
            symbolIconMap,
            // 自定义保存的值(saveSessionCallback)
            customConfig
        } = config;

        // 获取配置项
        const mapAttribute = chartStyle["mapProp"]["value"];

        // 地图组件初始化
        let map;
        if (customConfig != null && JSON.stringify(customConfig).length > 2) {
            // 读取保存的配置
            map = new AMap.Map(dom, {
                resizeEnable: true,
                center: [customConfig.lng, customConfig.lat],
                zoom: customConfig.zoom,
            });
        } else {
            map = new AMap.Map(dom, {
                resizeEnable: true,
                center: [mapAttribute[0], mapAttribute[1]],
                zoom: mapAttribute[2],
            });
        }

        // 地图设置样式
        const styleName = "amap://styles/" + mapAttribute[3];
        map.setMapStyle(styleName);

        // 获取全部的经纬度点数据
        const points = _getAllPoint(dataMapping, dataModels[0]);

        if (points != null) {
            // 创建聚合点图层
            var pointCluster = new AMap.MarkerCluster(map, points, {
                gridSize: 60, // 设置网格像素大小
                renderClusterMarker: _renderCarClusterMarker, // 自定义聚合点样式
                renderMarker: _renderMarker,   // 自定义非聚合点样式
            });
        }


        window.addEventListener("resize", function () {
            dom.style.width = document.body.clientWidth + "px";
            dom.style.height = document.body.clientHeight + "px";
        });

        document.querySelector("#amap-demo-click").onclick = function () {
            let conf = {
                zoom: map.getZoom(),
                lng: map.getCenter().lng,
                lat: map.getCenter().lat,
            };
            // 保存配置
            saveSessionCallback(conf);
            extensionCallBack('refresh');
        }

        /**
         * 自定义聚合点样式
         * @param context
         * @private
         */
        function _renderCarClusterMarker(context) {
            let count = context.count;
            let factor = context.count / count;
            const div = document.createElement('div');
            const Hue = 180 - factor * 180;
            let bgColor;
            if (context.count < 10) {
                bgColor = 'hsla(108,100%,40%,1)';
            } else if (context.count < 100) {
                bgColor = 'hsl(201,100%,40%)';
            } else if (context.count < 1000) {
                bgColor = 'hsla(36,100%,50%,1)';
            } else if (context.count < 10000) {
                bgColor = 'hsla(0,100%,60%,1)';
            } else {
                bgColor = 'hsla(0,100%,40%,1)';
            }
            const fontColor = 'hsla(' + Hue + ',100%,90%,1)';
            const borderColor = bgColor;
            const shadowColor = 'hsla(' + Hue + ',100%,90%,1)';
            div.style.backgroundColor = bgColor;
            const size = Math.round(30 + Math.pow(context.count / count, 1 / 5) * 20);
            div.style.width = div.style.height = size + 'px';
            div.style.border = 'solid 1px ' + borderColor;
            div.style.borderRadius = size / 2 + 'px';
            div.style.boxShadow = '0 0 5px ' + shadowColor;
            div.innerHTML = context.count;
            div.style.lineHeight = size + 'px';
            div.style.color = fontColor;
            div.style.fontSize = '14px';
            div.style.textAlign = 'center';
            context.marker.setOffset(new AMap.Pixel(-size / 2, -size / 2));
            context.marker.setContent(div)
        }

        /**
         * 自定义非聚合点样式
         * @param context
         * @private
         */
        function _renderMarker(context) {
            var content = '<div style="background-color: rgba(255,255,178,.9); height: 18px; width: 18px; border: 1px solid rgba(255,255,178,1); border-radius: 12px; box-shadow: rgba(0, 0, 0, 1) 0px 0px 3px;"></div>';
            var offset = new AMap.Pixel(-9, -9);
            context.marker.setContent(content)
            context.marker.setOffset(offset)
            let datum = context.data[0];
            // let id = datum.idId;
            // const val = [
            //     {
            //         dId: datum.idId,
            //         text: datum.idVal,
            //     },
            //     {
            //         dId: datum.lngId,
            //         text: datum.lngVal,
            //     },
            //     {
            //         dId: datum.latId,
            //         text: datum.latVal,
            //     }
            // ];

            // /**
            //  * dId: 当前维度的Id
            //  * value: 当前维度和之前维度的id及对应值  (不是全部的维度信息!)
            //  */
            // const currentClicked = {
            //     dId: id,
            //     value: val
            // };

            // context.marker.on('click', ev => {
            //     debugger;
            //     // 触发联动
            //     extensionCallBack(
            //         'click',
            //         currentClicked
            //     );
            // });

            const currentClicked = {
                [datum.idId]: datum.idVal,
                [datum.lngId]: datum.lngVal,
                [datum.latId]: datum.latVal
            };

            const currentId = datum.lngId
            context.marker.on('click', ev => {
                debugger;
                // 触发联动
                const demo = {
                    pos: {
                        x: window.event.pageX,
                        y: window.event.pageY,
                    },
                    // 点击指标的维度id
                    measure: currentId,
                    // 点击指标所在行的各个字段值,包括指标和维度
                    // id: 字段值
                    row: currentClicked,
                };
                // 点击指标事件(数据栏里是指标【绿色的】使用该方法)
                // extensionCallBack("pointSelected", demo);
                // 点击维度事件(数据栏里是维度【蓝色的】使用该方法)
                // extensionCallBack("dimensionSelected", demo);
                for (let i = 0; i < data.dataModels[0].fields.length; i++) {
                    let field = data.dataModels[0].fields[i];
                    if (field.id === currentId) {
                        // 这里判断用户的数据栏里是维度还是指标
                        if (field.isDimension) {
                            extensionCallBack("dimensionSelected", demo);
                        } else if (field.isMeasure) {
                            extensionCallBack("pointSelected", demo);
                        }
                        break;
                    }
                }
            });

        }

    }

    /**
     *
     * @param dataMapping
     * @param dataModel
     * @returns {*[]}
     * @private
     */
    function _getAllPoint(dataMapping, dataModel) {
        let lngId = dataMapping['lng'];
        let latId = dataMapping['lat'];

        let files = dataModel.fields;
        let rowCount = dataModel.rowCount
        const colData = dataModel.colData;

        let lngIndex;
        let latIndex;
        let idIndex;

        // demo这里没有考虑多维度多指标的情况,具体根据实际情况进行修改
        files.forEach((item, index) => {
            if (lngId.indexOf(item.id) >= 0) {
                lngIndex = index;
            } else if (latId.indexOf(item.id) >= 0) {
                latIndex = index;
            } else {
                idIndex = index;
            }
        })

        var points = [];
        for (let i = 0; i < rowCount; i++) {
            points.push({
                lnglat: [colData[lngIndex][i], colData[latIndex][i]],
                lngId: files[lngIndex].id,
                latId: files[latIndex].id,
                idId: files[idIndex].id,
                lngVal: colData[lngIndex][i],
                latVal: colData[latIndex][i],
                idVal: colData[idIndex][i],
            })
        }

        return points;
    }

    // 注册渲染方法
    new BIPlugin().init(render);
})(jQuery);

效果图

DEMO

上述效果图DEMO源码