diff --git a/capabilities.json b/capabilities.json index d32caef..c93e05b 100644 --- a/capabilities.json +++ b/capabilities.json @@ -1,84 +1,73 @@ { - "dataRoles": [ - { - "displayName": "Category Data", - "name": "category", - "kind": "Grouping" - }, - { - "displayName": "Measure Data", - "name": "measure", - "kind": "Measure" - } - ], + "dataRoles": [{ + "displayName": "urls", + "name": "urls", + "kind": "Grouping", + "description": "Urls of pictures, example: 'http://www.example.com/xx.png'" + }], "objects": { - "dataPoint": { - "displayName": "Data colors", + "layout": { + "displayName": "layout", "properties": { - "defaultColor": { - "displayName": "Default color", + "rowsCount": { "type": { - "fill": { - "solid": { - "color": true - } - } - } + "numeric": true + }, + "displayName": "rowsCount", + "description": "How many rows?" }, - "showAllDataPoints": { - "displayName": "Show all", + "rowGap": { "type": { - "bool": true - } + "numeric": true + }, + "placeHolderText": "{value}%", + "displayName": "rowGap", + "description": "The distance moved during each cycle.(%)" }, - "fill": { - "displayName": "Fill", + "columnGap": { "type": { - "fill": { - "solid": { - "color": true - } - } - } - }, - "fillRule": { - "displayName": "Color saturation", - "type": { - "fill": {} - } - }, - "fontSize": { - "displayName": "Text Size", - "type": { - "formatting": { - "fontSize": true - } - } + "numeric": true + }, + "placeHolderText": "msec", + "displayName": "columnGap", + "description": "How long a cycle takes? (msec)" } + + } + }, + "animation": { + "displayName": "animation", + "properties": { + "feet": { + "type": { + "numeric": true + }, + "placeHolderText": "{value}%", + "displayName": "feet", + "description": "The distance moved during each cycle.(%)" + }, + "interval": { + "type": { + "numeric": true + }, + "placeHolderText": "numeric", + "displayName": "interval", + "description": "How long a cycle takes? (msec)" + } + } } }, - "dataViewMappings": [ - { - "categorical": { - "categories": { - "for": { - "in": "category" - }, - "dataReductionAlgorithm": { - "top": {} - } + "dataViewMappings": [{ + "categorical": { + "categories": { + "for": { + "in": "urls" }, - "values": { - "select": [ - { - "bind": { - "to": "measure" - } - } - ] + "dataReductionAlgorithm": { + "top": {} } } } - ] -} + }] +} \ No newline at end of file diff --git a/pbiviz.json b/pbiviz.json index 578e268..84e86d2 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -1 +1,25 @@ -{"visual":{"name":"picsScroller","displayName":"picsScroller","guid":"picsScroller4F6E5B21FAB94F2081F33542CFAD9E09","visualClassName":"Visual","version":"1.0.0","description":"","supportUrl":"","gitHubUrl":""},"apiVersion":"2.6.0","author":{"name":"","email":""},"assets":{"icon":"assets/icon.png"},"externalJS":null,"style":"style/visual.less","capabilities":"capabilities.json","dependencies":null,"stringResources":[]} +{ + "visual": { + "name": "picsScroller", + "displayName": "picsScroller", + "guid": "picsScroller4F6E5B21FAB94F2081F33542CFAD9E09", + "visualClassName": "Visual", + "version": "1.0.0", + "description": "scroll pictures", + "supportUrl": "https://blog.mujiannan.me", + "gitHubUrl": "https://www.github.com/mujiannan" + }, + "apiVersion": "2.6.0", + "author": { + "name": "shennan", + "email": "littlesand@Outlook.com" + }, + "assets": { + "icon": "assets/icon.png" + }, + "externalJS": [], + "style": "style/visual.less", + "capabilities": "capabilities.json", + "dependencies": null, + "stringResources": [] +} \ No newline at end of file diff --git a/src/css/picsScroller.css b/src/css/picsScroller.css new file mode 100644 index 0000000..95c1fca --- /dev/null +++ b/src/css/picsScroller.css @@ -0,0 +1,31 @@ +div.pics-sroller-inner-container { + position: absolute; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +div.pics-scroller-row-container { + width: 100%; + overflow-x: hidden; + overflow-y: nowrap; + white-space: nowrap; + display: flex; + flex-direction: row; +} + +div.pics-scroller-img-container { + height: 100%; + width: auto; + display: inline-block; +} + +div.pics-scroll-invisible-img-container { + display: none; +} + +img.pics-scroller-img { + height: 100%; +} \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts index 002889b..b5ce74b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -30,19 +30,16 @@ import { dataViewObjectsParser } from "powerbi-visuals-utils-dataviewutils"; import DataViewObjectsParser = dataViewObjectsParser.DataViewObjectsParser; export class VisualSettings extends DataViewObjectsParser { - public dataPoint: dataPointSettings = new dataPointSettings(); - } - - export class dataPointSettings { - // Default color - public defaultColor: string = ""; - // Show all - public showAllDataPoints: boolean = true; - // Fill - public fill: string = ""; - // Color saturation - public fillRule: string = ""; - // Text Size - public fontSize: number = 12; - } + public animation: AnimationSettings = new AnimationSettings(); + public layout:LayoutSettings=new LayoutSettings(); +} +export class AnimationSettings { + public feet:number=1;//n% * width + public interval:number=20;//ms +} +export class LayoutSettings{ + public rowsCount:number=1; + public rowGap:number=2;//n% * height + public columnGap:number=2;//n% * width +} diff --git a/src/visual.ts b/src/visual.ts index 3644f83..7bd169a 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -27,6 +27,7 @@ import "core-js/stable"; import "./../style/visual.less"; +import "./css/picsScroller.css"; import powerbi from "powerbi-visuals-api"; import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; @@ -37,34 +38,130 @@ import DataView = powerbi.DataView; import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject; import { VisualSettings } from "./settings"; +import { timeout } from "d3"; + + +export interface IPicsScrollerData { + urls: string[]; +} export class Visual implements IVisual { - private target: HTMLElement; - private updateCount: number; + private container: HTMLElement; private settings: VisualSettings; - private textNode: Text; - + private innerContainer!: HTMLDivElement; + private animationPlaying: boolean = true;//控制动画暂停 constructor(options: VisualConstructorOptions) { - console.log('Visual constructor', options); - this.target = options.element; - this.updateCount = 0; - if (document) { - const new_p: HTMLElement = document.createElement("p"); - new_p.appendChild(document.createTextNode("Update count:")); - const new_em: HTMLElement = document.createElement("em"); - this.textNode = document.createTextNode(this.updateCount.toString()); - new_em.appendChild(this.textNode); - new_p.appendChild(new_em); - this.target.appendChild(new_p); - } + this.container = options.element; + } + private initial() { + if (this.innerContainer) { + this.innerContainer.remove(); + }; + this.innerContainer = document.createElement("div"); + this.innerContainer.className = "pics-sroller-inner-container"; + this.container.appendChild(this.innerContainer); } - public update(options: VisualUpdateOptions) { this.settings = Visual.parseSettings(options && options.dataViews && options.dataViews[0]); - console.log('Visual update', options); - if (this.textNode) { - this.textNode.textContent = (this.updateCount++).toString(); + this.initial(); + + let data: IPicsScrollerData = { urls: options.dataViews[0].categorical.categories[0].values }; + let feet = options.viewport.width / 100 * this.settings.animation.feet; + let interval = this.settings.animation.interval; + let rowGap = options.viewport.height / 100 * this.settings.layout.rowGap; + let columnGap = options.viewport.width / 100 * this.settings.layout.columnGap; + let rowsCount=this.settings.layout.rowsCount; + //处理输入 + if (!(data.urls.length > 0) || !(rowsCount! > 0)) { + console.info("picsScroller error: (source_url.length,rowsCount) ", data.urls.length + "_" + rowsCount); + return; + }; + if (rowsCount > data.urls.length) { + rowsCount = data.urls.length; + }; + let brandsCount = data.urls.length; + let unitsCountPerRow = Math.floor(brandsCount / rowsCount); + let logoUrlsArr: string[][] = []; + + //转换url列表形态 + for (let i = 0; i < rowsCount; i++) { + logoUrlsArr.push(data.urls.slice(i * unitsCountPerRow, Math.min((i + 1) * unitsCountPerRow, data.urls.length))); + }; + //分成rowsCount行 + for (let rowNum = 0; rowNum < rowsCount; rowNum++) { + let rowContainer = document.createElement('div'); + this.innerContainer.appendChild(rowContainer); + rowContainer.classList.add('pics-scroller-row-container'); + if (rowNum > 0) { + rowContainer.style.marginTop = rowGap + "px"; + }; + rowContainer.setAttribute('id', 'pics-scroller-row-container' + rowNum); + rowContainer.setAttribute("style", "height:" + 100 / rowsCount + "%;"); + rowContainer.setAttribute("data-animation-playing", 'false'); + rowContainer.setAttribute("data-display-placehold", "false"); + + //双倍图片容器,第一组用来显示,第二组放上去暂时隐藏 + //如果第一组发生了溢出,就显示出第二组并应用滚动动画 + for (let i = 0; i < 2; i++) { + for (let j = 0; j < logoUrlsArr[rowNum].length; j++) { + let logoContainer = document.createElement('div'); + logoContainer.className = "pics-scroller-img-container"; + if (i == 1) { + logoContainer.classList.add("pics-scroll-invisible-img-container"); + }; + let logo = document.createElement('img'); + logo.style.margin = "0px " + columnGap / 2 + "px"; + logo.setAttribute("class", "pics-scroller-img"); + logo.setAttribute("src", logoUrlsArr[rowNum][j]); + logoContainer.appendChild(logo); + rowContainer.appendChild(logoContainer); + }; + } + + //重新激活动画 + if (this.animationPlaying) { + this.activateAnimation(); + } + //假无限滚动 + window.setInterval(this.scrollLeftInfinity, interval, rowContainer, feet); + }; + + } + public activateAnimation() { + this.animationPlaying = true; + let rowContainers = this.innerContainer.getElementsByClassName("pics-scroller-row-container"); + for (let i = 0; i < rowContainers.length; i++) { + rowContainers[i].setAttribute("data-animation-playing", "true"); } } + private suspendAnimation() { + let rowContainers = this.innerContainer.getElementsByClassName("pics-scroller-row-container"); + for (let i = 0; i < rowContainers.length; i++) { + rowContainers[i].setAttribute("data-animation-playing", "false"); + } + } + //定义无限向左滚动函数 + private scrollLeftInfinity(obj: HTMLDivElement, feet: number) { + let playAnimation = obj.getAttribute("data-animation-playing"); + let displayPlaceHold = obj.getAttribute("data-display-placehold"); + if (playAnimation == "false") { + return; + } else if (playAnimation == "true" && displayPlaceHold == "false" && obj.scrollWidth > obj.clientWidth) { + let invisibleImgContainers = obj.getElementsByClassName("pics-scroll-invisible-img-container"); + while (invisibleImgContainers.length > 0) { + invisibleImgContainers[0].classList.remove("pics-scroll-invisible-img-container"); + } + obj.setAttribute("data-display-placehold", "true"); + } + if (obj.scrollWidth > obj.clientWidth) { + if (obj.scrollLeft >= obj.scrollWidth / 2) { + obj.scrollLeft = 0; + } else { + obj.scrollLeft += feet; + }; + } + + } + private static parseSettings(dataView: DataView): VisualSettings { return VisualSettings.parse(dataView);