Forum Discussion
SmithCham
2 years agoRegular Visitor
PowerBI Cannot Open internalSaveWidget HTML file for custom plotly visual in PowerBI Service
I am currently trying to create a custom plotly visual in PowerBI. I have a working visual in PowerBI Desktop, but when I publish it to PowerBI Service, I get the following error: Error in...
SmithCham
2 years agoRegular Visitor
flatten_HTML.r is the following:
############### Utility functions ###############
libraryRequireInstall = function(packageName, ...)
{
if(!require(packageName, character.only = TRUE))
warning(paste("*** The package: '", packageName, "' was not installed ***", sep=""))
}
libraryRequireInstall("xml2")
libraryRequireInstall("htmlwidgets")
internalSaveWidget <- function(widget, fname)
{
tempFname = paste(fname, ".tmp", sep="")
htmlwidgets::saveWidget(widget, file = tempFname, selfcontained = FALSE)
FlattenHTML(tempFname, fname)
}
FlattenHTML <- function(fnameIn, fnameOut)
{
# Read and parse HTML file
# Embed all js and css files into one unified file
if(!file.exists(fnameIn))
return(FALSE)
dir = dirname(fnameIn)
html = read_html(fnameIn, useInternal = TRUE)
top = xml_root(html)
# extract all <script> tags with src value
srcNode=xml_find_all(top, '//script[@src]')
for (node in srcNode)
{
b = xml_attrs(node)
fname = file.path(dir, b['src'])
alternatesrc=FindSrcReplacement(fname)
if (!is.null(alternateSrc))
{
s = alternateSrc
names(s) = 'src'
newNode = xml_new_root("script")
xml_set_attrs(newNode, s)
xml_replace(node, newNode)
}else{
str=ReadFileForEmbedding(fname);
if (!is.null(str))
{
newNode = xml_new_root("script",str)
xml_set_attrs( newNode, c( type = "text/javascript") )
xml_replace(node, newNode)
}
}
}
# extract all <link> tags with src value
linkNode=xml_find_all(top, '//link[@href]')
for (node in linkNode)
{
b = xml_attrs(node)
fname = file.path(dir, b['href'])
str = ReadFileForEmbedding(fname, FALSE);
if (!is.null(str))
{
newNode = xml_new_root("style", str)
xml_replace(node, newNode)
}
}
write_xml(html, file = fnameOut)
return(TRUE)
}
ReadFileForEmbedding <- function(fname, addCdata = TRUE)
{
data = ReadFullFile(fname)
if (is.null(data))
return(NULL)
str = paste(data, collapse ='\n')
if (addCdata) {
str = paste(cbind('// <![CDATA[', str,'// ]]>'), collapse ='\n')
}
return(str)
}
ReadFullFile <- function(fname)
{
if(!file.exists(fname))
return(NULL)
con = file(fname, open = "r")
data = readLines(con)
close(con)
return(data)
}
FindSrcReplacement <- function(str)
{
# finds reference to 'plotly' js and replaces with a version from CDN
# This allows the HTML to be smaller, since this script is not fully embedded in it
str <- iconv(str, to="UTF-8")
pattern = "plotly-(\\w.+)/plotly-latest.min.js"
match1=regexpr(pattern, str)
attr(match1, 'useBytes') <- FALSE
strMatch=regmatches(str, match1, invert = FALSE)
if (length(strMatch) == 0) return(NULL)
pattern2 = "-(\\d.+)/"
match2 = regexpr(pattern2, strMatch[1])
attr(match2, 'useBytes') <- FALSE
strmatch = regmatches(strMatch[1], match2)
if (length(strmatch) == 0) return(NULL)
# CDN url is https://cdn.plot.ly/plotly-<Version>.js
# This matches the specific version used in the plotly package used.
verstr = substr(strmatch, 2, nchar(strmatch)-1)
str = paste('https://cdn.plot.ly/plotly-', verstr,'.min.js', sep='')
return(str)
}
ReadFullFileReplaceString <- function(fnameIn, fnameOut, sourceString,targetString) {
# Replaces texts in file
# This makes it possible to replace e.g. paddings in the generated html widget code
if(!file.exists(fnameIn))
return(NULL)
tx <- readLines(fnameIn,encoding = "UTF-8")
tx2 <- gsub(pattern = sourceString, replace = targetString, x = tx)
writeLines(tx2, con = fnameOut)
}
#################################################
visual.ts and htmlInjectionUtility I believe are also key components in reading/rendering the visual. visual.ts is:
/*
* Power BI Visual CLI
*
* Copyright (c) Microsoft Corporation
* All rights reserved.
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the ""Software""), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
"use strict";
import powerbi from "powerbi-visuals-api";
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions;
import VisualObjectInstance = powerbi.VisualObjectInstance;
import DataView = powerbi.DataView;
import IViewport = powerbi.IViewport;
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject;
import { VisualSettings } from "./settings";
import { parseElement, resetInjector, runHTMLWidgetRenderer } from "./htmlInjectionUtility";
enum VisualUpdateType {
Data = 2,
Resize = 4,
ViewMode = 8,
Style = 16,
ResizeEnd = 32,
All = 62,
}
// below is a snippet of a definition for an object which will contain the property values
// selected by the users
/*interface VisualSettings {
lineColor: string;
}*/
// to allow this scenario you should first the following JSON definition to the capabilities.json file
// under the "objects" property:
// "settings": {
// "displayName": "Visual Settings",
// "description": "Visual Settings Tooltip",
// "properties": {
// "lineColor": {
// "displayName": "Line Color",
// "type": { "fill": { "solid": { "color": true }}}
// }
// }
// }
// in order to improve the performance, one can update the <head> only in the initial rendering.
// set to 'true' if you are using different packages to create the widgets
const updateHTMLHead: boolean = false;
const renderVisualUpdateType: number[] = [
VisualUpdateType.Resize,
VisualUpdateType.ResizeEnd,
VisualUpdateType.Resize + VisualUpdateType.ResizeEnd
];
export class Visual implements IVisual {
private rootElement: HTMLElement;
private headNodes: Node[];
private bodyNodes: Node[];
private settings: VisualSettings;
public constructor(options: VisualConstructorOptions) {
if (options && options.element) {
this.rootElement = options.element;
}
this.headNodes = [];
this.bodyNodes = [];
}
public update(options: VisualUpdateOptions): void {
if (!options ||
!options.type ||
!options.viewport ||
!options.dataViews ||
options.dataViews.length === 0 ||
!options.dataViews[0]) {
return;
}
const dataView: DataView = options.dataViews[0];
this.settings = Visual.parseSettings(dataView);
let payloadBase64: string = null;
if (dataView.scriptResult && dataView.scriptResult.payloadBase64) {
payloadBase64 = dataView.scriptResult.payloadBase64;
}
if (renderVisualUpdateType.indexOf(options.type) === -1) {
if (payloadBase64) {
this.injectCodeFromPayload(payloadBase64);
}
} else {
this.onResizing(options.viewport);
}
}
public onResizing(finalViewport: IViewport): void {
// tslint:disable-next-line
/* add code to handle resizing of the view port */
}
private injectCodeFromPayload(payloadBase64: string): void {
// inject HTML from payload, created in R
// the code is injected to the 'head' and 'body' sections.
// if the visual was already rendered, the previous DOM elements are cleared
resetInjector();
if (!payloadBase64) {
return;
}
// create 'virtual' HTML, so parsing is easier
let el: HTMLHtmlElement = document.createElement("html");
try {
// tslint:disable-next-line
el.innerHTML = window.atob(payloadBase64);
} catch (err) {
return;
}
// if 'updateHTMLHead == false', then the code updates the header data only on the 1st rendering
// this option allows loading and parsing of large and recurring scripts only once.
if (updateHTMLHead || this.headNodes.length === 0) {
while (this.headNodes.length > 0) {
let tempNode: Node = this.headNodes.pop();
document.head.removeChild(tempNode);
}
let headList: HTMLCollectionOf<HTMLHeadElement> = el.getElementsByTagName("head");
if (headList && headList.length > 0) {
let head: HTMLHeadElement = headList[0];
this.headNodes = parseElement(head, document.head);
}
}
// update 'body' nodes, under the rootElement
while (this.bodyNodes.length > 0) {
let tempNode: Node = this.bodyNodes.pop();
this.rootElement.removeChild(tempNode);
}
let bodyList: HTMLCollectionOf<HTMLBodyElement> = el.getElementsByTagName("body");
if (bodyList && bodyList.length > 0) {
let body: HTMLBodyElement = bodyList[0];
this.bodyNodes = parseElement(body, this.rootElement);
}
runHTMLWidgetRenderer();
}
private static parseSettings(dataView: DataView): VisualSettings {
return <VisualSettings>VisualSettings.parse(dataView);
}
/**
* 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);
}
}
And htmlINjectionUtility is:
"use strict";
let injectorCounter: number = 0;
export function resetInjector(): void {
injectorCounter = 0;
}
export function injectorReady(): boolean {
return injectorCounter === 0;
}
export function parseElement(el: HTMLElement, target: HTMLElement): Node[] {
let arr: Node[] = [];
if (!el || !el.hasChildNodes()) {
return;
}
let nodes: HTMLCollection = el.children;
for (let i: number = 0; i < nodes.length; i++) {
let tempNode: HTMLElement;
if (nodes.item(i).nodeName.toLowerCase() === "script") {
tempNode = createScriptNode(nodes.item(i));
} else {
tempNode = <HTMLElement>nodes.item(i).cloneNode(true);
}
target.appendChild(tempNode);
arr.push(tempNode);
}
return arr;
}
function createScriptNode(refNode: Element): HTMLElement {
let script: HTMLScriptElement = document.createElement("script");
let attr: NamedNodeMap = refNode.attributes;
for (let i: number = 0; i < attr.length; i++) {
script.setAttribute(attr[i].name, attr[i].textContent);
if (attr[i].name.toLowerCase() === "src") {
// waiting only for src to finish loading - async opetation
injectorCounter++;
script.onload = () => {
injectorCounter--;
};
}
}
// tslint:disable-next-line
script.innerHTML = refNode.innerHTML;
return script;
}
export function runHTMLWidgetRenderer(): void {
// rendering HTML which was created by HTMLWidgets package
// wait till all tje script elements are loaded
let intervalVar: number = window.setInterval(() => {
if (injectorReady()) {
window.clearInterval(intervalVar);
if (window.hasOwnProperty("HTMLWidgets") && window["HTMLWidgets"].staticRender) {
window["HTMLWidgets"].staticRender();
}
}
}, 100);
}
- lbendlin2 years agoSuper User
Yeah, looks like this is trying to read and write from/to local storage. Pretty sure that is not supported in Power BI.