Power BI 3D条形图自定义视觉不绘制数据

时间:2018-09-10 12:48:40

标签: typescript powerbi

我正在尝试获取3D条形图以用作自定义视觉效果。它应绘制的数据是两个类别变量(X和Y),用于获取图表的网格,以及用于定义条形高度的数值(Z),例如:3D bar chart

视觉效果很好,没有任何错误,但是当我尝试在Power BI Service中显示视觉效果时,只会返回empty visual

我认为问题可能出在数据绑定中,例如视觉图无法绘制在PBI中选择的数据。 但是,我无法找到问题所在。

感谢您的帮助,谢谢!

以下是可视代码:

module powerbi.extensibility.visual {
"use strict";

// import ColorHelper = powerbi.extensibility.utils.color.ColorHelper;
import IColorPalette = powerbi.extensibility.IColorPalette;

interface BarMeshParams {
    width: number;
    height: number;
    depth: number;
    x: number;
    y: number;
    z?: number;
    color?: string;
}

interface CameraPosition {
    x: number;
    y: number;
    z: number;
    rotationX: number;
    rotationY: number;
    rotationZ: number;
}

enum Axis {
    X,
    Y,
    Z
}

export class Visual implements IVisual {
    private target: HTMLElement;
    private settings: VisualSettings;

    private cameraControl: CameraControl;
    private controls: THREE.OrbitControls;
    private scene: THREE.Scene;
    private camera: THREE.Camera;
    private renderer: THREE.Renderer;
    private parent3D: THREE.Object3D;
    private colorPalette: IColorPalette;

    public static CategoryXIndex: number = 1;
    public static CategoryYIndex: number = 0;
    public static DataViewIndex: number = 0;
    public static ValuesIndex: number = 0;
    private static CameraDefaultPosition: CameraPosition = <CameraPosition>{
        z: 10,
        x: 5,
        y: 0,
        rotationX: 0,
        rotationY: 0,
        rotationZ: 0
    };

    private dataPassedFlag: boolean;
    private host: IVisualHost;

    constructor(options: VisualConstructorOptions) {
        console.log('Visual constructor', options);
        this.target = options.element;
        this.host = options.host;

        this.scene = new THREE.Scene();
        this.configureCamera();
        this.renderer = new THREE.WebGLRenderer({
            alpha: true
        });
        // this.renderer.setClearColor( 0x000000, 0 ); // the default
        this.target.appendChild(this.renderer.domElement);
        this.colorPalette = options.host.colorPalette;

        let timeout: number = 0;
        if (typeof THREE.OrbitControls !== "undefined") {
            console.log('OrbitControls enabled');
            this.controls = new THREE.OrbitControls( this.camera, this.target );
            this.controls.addEventListener("change", () => {
                console.log(`position ${this.camera.position.x} ${this.camera.position.y} ${this.camera.position.z}`);
                console.log(`rotation ${this.camera.rotation.x} ${this.camera.rotation.y} ${this.camera.rotation.z}`);

                this.settings.cameraPosition.positionX = this.camera.position.x;
                this.settings.cameraPosition.positionY = this.camera.position.y;
                this.settings.cameraPosition.positionZ = this.camera.position.z;

                this.settings.cameraPosition.rotationX = this.camera.rotation.x;
                this.settings.cameraPosition.rotationY = this.camera.rotation.y;
                this.settings.cameraPosition.rotationZ = this.camera.rotation.z;
                // for prevent ddosing host
                if (timeout === 0) {
                    timeout = setTimeout(() => {
                        this.persistCameraSettings(this.camera.position, this.camera.rotation);
                        timeout = 0;
                    }, 3000);
                }
            })
            this.controls.update();
        }
        this.cameraControl = new CameraControl(this.renderer, <THREE.PerspectiveCamera>this.camera, () => {
            // you might want to rerender on camera update if you are not rerendering all the time
            window.requestAnimationFrame(() => this.renderer.render(this.scene, this.camera));
        })
        this.dataPassedFlag = false;
    }

    private persistCameraSettings(position: THREE.Vector3, rotation: THREE.Euler) {
        console.log('persist', position, rotation);
        const instance: powerbi.VisualObjectInstance = {
            objectName: "cameraPosition",
            selector: undefined,
            properties: {
                positionX: position.x,
                positionY: position.y,
                positionZ: position.z,
                rotationX: rotation.x,
                rotationY: rotation.y,
                rotationZ: rotation.z
            }
        };

        this.host.persistProperties({
            merge: [
                instance
            ]
        });
    }

    public clearScene(): void {
        while (this.scene.children.length > 0) {
            this.scene.remove(this.scene.children[0]);
        }
    }

    public update(options: VisualUpdateOptions) {
          if (
            options.type === VisualUpdateType.Data ||
            options.type === VisualUpdateType.All
        ) {
            this.clearScene();
        }
        if (!this.checkDataView(options.dataViews)) {
            this.dataPassedFlag = false;
            return;
        }

        let model = this.convertData(options.dataViews);
        if (model == null) {
            return;
        }

        this.settings = Visual.parseSettings(options && options.dataViews && options.dataViews[0]);
        this.configureCamera();

        if (
            options.type === VisualUpdateType.Resize ||
            options.type === VisualUpdateType.All ||
            !this.dataPassedFlag // correct data passed to the visual first time, the visual should configure the viewport
        ) {
            let width = options.viewport.width;
            let height = options.viewport.height;
            this.renderer.setSize( width, height);
        }
        this.dataPassedFlag = true;

        if (
            options.type === VisualUpdateType.Data ||
            options.type === VisualUpdateType.All
        ) {
            this.configureLights();

            this.drawBars(model);
            this.create2DLabels(model, Axis.X);
            this.create2DLabels(model, Axis.Y);
        }

        let this_ = this;
        function render() {
            requestAnimationFrame( render );
            this_.controls.update();
            this_.renderer.render( this_.scene, this_.camera );
            this_.controls.update();
        }
        render();

    }

    private createBar(params: BarMeshParams, includeToScene: boolean = false): THREE.Mesh {

        params.color = params.color || "red";
        params.y = params.y || 0;
        let boxGeometry = new THREE.BoxGeometry(params.width, params.height, params.depth);
        boxGeometry.translate(0, 0 , params.depth / 2.0);
        let material = new THREE.MeshLambertMaterial( {
            color: params.color
        });
        let cube = new THREE.Mesh(boxGeometry, material);
        cube.position.x = params.x;
        cube.position.y = params.y;
        cube.position.z = params.z;
        if (includeToScene) {
            this.scene.add(cube);
        }
        return cube;

    }

    private drawBars(model: Bar3DChartDataModel): void {
        let bar1 = this.createBar({ width: 1, height: 1, depth: 1, x: 1, y: 1, z: 0, color: "blue" });
        let scale: d3.scale.Linear<number, number> = d3.scale.linear().domain([0, model.maxLocal]).range([0, BAR_SIZE_HEIGHT]);
        model.bars.forEach((bar: Bar3D) => {
            let barMesh = this.createBar({
                width: BAR_SIZE,
                height: scale(bar.value),
                depth: BAR_SIZE,
                x: bar.x,
                z: bar.z,
                y: scale(bar.value) / 2,
                color: bar.color
            });
            this.scene.add(barMesh);
        });

    }

    private shiftCameraToCenterOfChart(model: Bar3DChartDataModel) {
        // TODO fix
    }

    public static degRad(deg: number): number {
        return deg * Math.PI / 180;
    }

    private configureCamera(): void {
        if (!this.camera) {
            this.camera = new THREE.PerspectiveCamera( 75, 800 / 600, 0.1, 1000 );
        }
        let defaultCameraSettings = new CameraPosition();

        let positions = this.settings && this.settings.cameraPosition || defaultCameraSettings;
        this.camera.position.set(positions.positionX, positions.positionY, positions.positionZ);
        this.camera.rotation.set(positions.rotationX, positions.rotationY, positions.rotationZ);
    }

    private configureLights(): void {
        let hemiLight = new THREE.HemisphereLight( COLOR_WHITE, COLOR_WHITE, 0.6 );
        hemiLight.color.setHSL( 0.6, 0.75, 0.5 );
        hemiLight.groundColor.setHSL( 0.095, 0.5, 0.5 );
        hemiLight.position.set( 0, 500, 0 );
        this.scene.add( hemiLight );

        let dirLight = new THREE.DirectionalLight( COLOR_WHITE, 1 );
        dirLight.position.set( 5, -5, 8 );
        dirLight.position.multiplyScalar( 50);
        dirLight.name = "dirlight";
        // dirLight.shadowCameraVisible = true;

        this.scene.add( dirLight );

        dirLight.castShadow = true;
        dirLight.shadowMapWidth = dirLight.shadowMapHeight = 1024 * 2;

        let d = 300;

        dirLight.shadowCameraLeft = -d;
        dirLight.shadowCameraRight = d;
        dirLight.shadowCameraTop = d;
        dirLight.shadowCameraBottom = -d;

        dirLight.shadowCameraFar = 3500;
        dirLight.shadowBias = -0.0001;
        // dirLight.shadowDarkness = 0.35;
    }

    private configureParentObject() {
    }

    private static parseSettings(dataView: DataView): VisualSettings {
        return VisualSettings.parse(dataView) as VisualSettings;
    }

    private checkDataView(dataViews: DataView[]): boolean {
        if (!dataViews
        || !dataViews[Visual.DataViewIndex]
        || !dataViews[Visual.DataViewIndex].categorical
        || !dataViews[Visual.DataViewIndex].categorical.categories
        || !dataViews[Visual.DataViewIndex].categorical.categories[Visual.CategoryXIndex].source
        || !dataViews[Visual.DataViewIndex].categorical.categories[Visual.CategoryYIndex].source
        || !dataViews[Visual.DataViewIndex].categorical.values)
            return false;

        return true;
    }

    private convertData(dataViews: DataView[]): Bar3DChartDataModel {
        if (!this.checkDataView(dataViews)) {
            return null;
        }

        let categorical = dataViews[Visual.DataViewIndex].categorical;
        let categoryX = categorical.categories[Visual.CategoryXIndex];
        let categoryY = categorical.categories[Visual.CategoryYIndex];
        let dataValue = categorical.values[Visual.ValuesIndex];

        let xCategoryIndex: CategoryIndex = {};
        let yCategoryIndex: CategoryIndex = {};

        _.uniq(categoryX.values).forEach( (category, index) => {
            if (category === null) {
                category = "null";
            }
            xCategoryIndex[<string>category] = index;
        });
        _.uniq(categoryY.values).forEach( (category, index) => {
            if (category === null) {
                category = "null";
            }
            yCategoryIndex[<string>category] = index;
        });

        let bars: Bar3D[] = [];
        for (let valueIndex = 0; valueIndex < dataValue.values.length; valueIndex++) {
            let bar: Bar3D = <Bar3D>{
                categoryX: categoryX.values[valueIndex],
                categoryY: categoryY.values[valueIndex],
                value: dataValue.values[valueIndex],
                x: xCategoryIndex[<string>categoryX.values[valueIndex]],
                z: yCategoryIndex[<string>categoryY.values[valueIndex]],
                color: this.colorPalette.getColor(valueIndex.toString()).value
            };

            bars.push(bar);
        }

        // TODO sort bars by X and Y and indexes by value
        return <Bar3DChartDataModel>{
            bars: bars,
            categoryIndexX: xCategoryIndex,
            categoryIndexY: yCategoryIndex,
            minLocal: dataValue.minLocal,
            maxLocal: dataValue.maxLocal
        };
    }

    private draw2DLine() {
        //create a blue LineBasicMaterial
        let material = new THREE.LineBasicMaterial( { color: 0x0000ff } );
        let geometry = new THREE.Geometry();
        geometry.vertices.push(new THREE.Vector3( -10, 0, 0) );
        geometry.vertices.push(new THREE.Vector3( 0, 10, 0) );
        geometry.vertices.push(new THREE.Vector3( 10, 0, 0) );
        let line = new THREE.Line( geometry, material );
        line.position.x = 0;
        line.position.y = 0;
        line.position.z = 0;
        this.scene.add(line);
    }

    private create2DLabels(category: Bar3DChartDataModel, axis: Axis): void {
        let loader = new THREE.FontLoader();
        let values: CategoryIndex;
        let labelsShift: number;
        if (axis === Axis.X) {
            values = category.categoryIndexX;
            labelsShift = Object.keys(category.categoryIndexY).length * BAR_SIZE;
        }
        if (axis === Axis.Y) {
            values = category.categoryIndexY;
            labelsShift = Object.keys(category.categoryIndexX).length * BAR_SIZE;
        }
        loader.load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/fonts/helvetiker_regular.typeface.json', ( font ) => {
            Object.keys(values).forEach( (value: PrimitiveValue, index: number) => {
                let categoryLabel: THREE.TextGeometry = new THREE.TextGeometry( (value || "").toString(), {
                    font: new THREE.Font((<any>font).data),
                    height: 0.0001,
                    size: BAR_SIZE / 2.1,
                    bevelEnabled: false,
                    bevelSize: 1,
                    bevelThickness: 1
                });
                let material = new THREE.MeshLambertMaterial( {
                    color: "black"
                });

                let textMesh = new THREE.Mesh(categoryLabel, material);
                if (axis === Axis.Y) {
                    textMesh.position.x = BAR_SIZE + labelsShift;
                    textMesh.position.z = index + (1 - BAR_SIZE) + (BAR_SIZE / 2);
                    textMesh.position.y = 0;
                    textMesh.rotation.x = Visual.degRad(90);
                    textMesh.rotation.z = Visual.degRad(180);
                    textMesh.rotation.y = Visual.degRad(-180);
                    this.scene.add(textMesh);
                }
                if (axis === Axis.X) {
                    textMesh.geometry.computeBoundingBox();
                    let size: THREE.Vector3 = textMesh.geometry.boundingBox.getSize();
                    textMesh.position.z = BAR_SIZE + labelsShift + size.x;
                    textMesh.position.x = index + (1 - BAR_SIZE) * 2;
                    textMesh.position.y = 0;

                    textMesh.rotation.z = Visual.degRad(-90);
                    textMesh.rotation.x = Visual.degRad(90);
                    textMesh.rotation.y = Visual.degRad(180);
                    this.scene.add(textMesh);
                }
                this.scene.add(textMesh);
            });
        });
    }

    /**
     * This function gets called for each of the objects defined in the capabilities files and allows you to select which of the
     * objects and properties you want to expose to the users in the property pane.
     *
     */
    public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
        return VisualSettings.enumerateObjectInstances(this.settings || VisualSettings.getDefault(), options);
    }
}

}

我从GitHub那里获得了代码

0 个答案:

没有答案