Compare commits

...

13 Commits
master ... dev

27 changed files with 965 additions and 124 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
/node_modules
/dist
/.tmp
/.vscode
/.vscode
webpack.statistics.*

26
ReadMe.md Normal file
View File

@ -0,0 +1,26 @@
# A utility slicer for Power Bi
Assign default selection to this slicer by messures, just use DAX!
## Tutorial
1. Click the top right corner of this slicer
1. Click "Edit"
1. Save&publish the report
## Functionalities
* Implemented
1. Dropdown-slicer for text field
1. Use DAX to assign **single** default selected items to the dropdown-slicer
1. Support async slicers
* Planned
1. List-slicer for text field
1. Use DAX to assign **one** default selected items to the list-slicer
1. Use DAX to assign **multiple** default selected items to the dropdown-slicer and list-slicer
1. Range-slicer for number field
1. Use DAX to assign default selected range to the range-slicer
1. Single-date-slicer for datetime field
1. Use DAX to assign default selected date to the single-date-slicer
1. Period-slicer for datetime field
1. Use DAX to assign default selected period to the period-slicer
## Known issures
1. Slicer components for number field and datetime field are in developing, just slicer for text field is usable now
1. The outline setting has no effectivity on dropdown items
1. I can't change background-color of dropdown items
1. This slicer wouldn't support bookmarks

View File

@ -1,4 +1,6 @@
{
"supportsSynchronizingFilterState": true,
"advancedEditModeSupport": 1,
"dataRoles": [
{
"displayName": "Field",
@ -44,11 +46,30 @@
}
],
"objects": {
"dataPoint": {
"displayName": "Data colors",
"general": {
"displayName": "General",
"displayNameKey": "Formatting-General",
"properties": {
"defaultColor": {
"displayName": "Default color",
"filter": {
"type": {
"filter": true
}
},
"selfFilter": {
"type": {
"filter": {
"selfFilter": false
}
}
}
}
},
"slicerHeader": {
"displayName": "Slicer header",
"displayNameKey":"Formatting-SicerHeader",
"properties": {
"fontColor": {
"displayName": "Font color",
"type": {
"fill": {
"solid": {
@ -57,6 +78,79 @@
}
}
},
"backgroundColor": {
"displayName": "Background color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"outline":{
"displayName": "Outline",
"type": {
"enumeration": [
{
"value": "0",
"displayName": "None",
"displayNameKey": "Formatting_Outline_None"
},
{
"value": "1",
"displayName": "Bottom only",
"displayNameKey": "Formatting_Outline_BottomOnly"
},
{
"value": "2",
"displayName": "Top only",
"displayNameKey": "Formatting_Outline_TopOnly"
},
{
"value": "3",
"displayName": "Left only",
"displayNameKey": "Formatting_Outline_LeftOnly"
},
{
"value": "4",
"displayName": "Right only",
"displayNameKey": "Formatting_Outline_RightOnly"
},
{
"value": "5",
"displayName": "Top + bottom",
"displayNameKey": "Formatting_Outline_TopAndBottom"
},
{
"value": "6",
"displayName": "Left + right",
"displayNameKey": "Formatting_Outline_LeftAndRight"
},
{
"value": "7",
"displayName": "Frame",
"displayNameKey": "Formatting_Outline_Frame"
}
]
}
},
"outlineColor":{
"displayName": "Outline color",
"type": {
"fill": {
"solid": {
"color":true
}
}
}
},
"outlineWeight":{
"displayName": "Outline weight",
"type":{
"integer": true
}
},
"showAllDataPoints": {
"displayName": "Show all",
"type": {
@ -88,6 +182,90 @@
}
}
}
},
"items":{
"displayName": "Items",
"displayNameKey": "Formatting_Items",
"properties": {
"fontColor": {
"displayName": "Font color",
"displayNameKey": "Formatting_Items_FontColor",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"backgroundColor": {
"displayName": "Background color",
"displayNameKey": "Formatting_Items_BackgroundColor",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"outline":{
"displayName": "Outline",
"displayNameKey": "Formatting_Items_Outline",
"type": {
"enumeration": [
{
"value": "0",
"displayName": "None",
"displayNameKey": "Formatting_Outline_None"
},
{
"value": "1",
"displayName": "Bottom only",
"displayNameKey": "Formatting_Outline_BottomOnly"
},
{
"value": "2",
"displayName": "Top only",
"displayNameKey": "Formatting_Outline_TopOnly"
},
{
"value": "3",
"displayName": "Left only",
"displayNameKey": "Formatting_Outline_LeftOnly"
},
{
"value": "4",
"displayName": "Right only",
"displayNameKey": "Formatting_Outline_RightOnly"
},
{
"value": "5",
"displayName": "Top + bottom",
"displayNameKey": "Formatting_Outline_TopAndBottom"
},
{
"value": "6",
"displayName": "Left + right",
"displayNameKey": "Formatting_Outline_LeftAndRight"
},
{
"value": "7",
"displayName": "Frame",
"displayNameKey": "Formatting_Outline_Frame"
}
]
}
},
"textSize":{
"displayName": "Text size",
"displayNameKey": "Formatting_Items_TextSize",
"type":{
"integer": true
}
}
}
}
},
"dataViewMappings": [

16
package-lock.json generated
View File

@ -286,6 +286,12 @@
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
"integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
},
"@types/node": {
"version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -881,6 +887,11 @@
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"powerbi-models": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/powerbi-models/-/powerbi-models-1.3.3.tgz",
"integrity": "sha512-F4O92yCpJ6thAlEOoG77cTSrQHlMrS70XxGbaCrlShNpVzVU5gyUdua1LUURZmERpoRjKPFMvQtAknkz4B7lAA=="
},
"powerbi-visuals-api": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/powerbi-visuals-api/-/powerbi-visuals-api-2.6.2.tgz",
@ -894,6 +905,11 @@
"resolved": "https://registry.npmjs.org/powerbi-visuals-utils-dataviewutils/-/powerbi-visuals-utils-dataviewutils-2.2.1.tgz",
"integrity": "sha512-Ai+TM1gj6DpAsNbn0IhOwUCAPfcaH4Z7y6Ow2OwAfbxNpELwQSF0S8D+vlJN2AoqV/ruQhnEngUC88mMFNyvJQ=="
},
"powerbi-visuals-utils-typeutils": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/powerbi-visuals-utils-typeutils/-/powerbi-visuals-utils-typeutils-2.2.1.tgz",
"integrity": "sha512-xm5xNBVudCiU9ZZggsLlpHr+a4bnHtgw6Cy1UtNM/zILtOE2HUamjw+yZovLe6YNov4N2EaCmPO8XPhcXkuz+A=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View File

@ -10,12 +10,15 @@
"@babel/runtime": "7.6.0",
"@babel/runtime-corejs2": "7.6.0",
"@types/d3": "5.7.2",
"core-js": "3.2.1",
"d3": "5.12.0",
"powerbi-visuals-utils-dataviewutils": "2.2.1",
"powerbi-models": "^1.3.3",
"powerbi-visuals-api": "~2.6.1",
"core-js": "3.2.1"
"powerbi-visuals-utils-dataviewutils": "2.2.1",
"powerbi-visuals-utils-typeutils": "^2.2.1"
},
"devDependencies": {
"@types/node": "^13.13.4",
"ts-loader": "6.1.0",
"tslint": "^5.18.0",
"tslint-microsoft-contrib": "^6.2.0",

View File

@ -1 +1,27 @@
{"visual":{"name":"syinpoSlicer","displayName":"SyinpoSlicer","guid":"syinpoSlicer1E14A7D0BD9E4C9B99612FBE9E84697A","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": "syinpoSlicer",
"displayName": "SyinpoSlicer",
"guid": "syinpoSlicer1E14A7D0BD9E4C9B99612FBE9E84697A",
"visualClassName": "Visual",
"version": "1.0.0",
"description": "A slicer, default selected according to messures.",
"supportUrl": "https://github.com/mujiannan/syinpoSlicer",
"gitHubUrl": "https://github.com/mujiannan/syinpoSlicer"
},
"apiVersion": "2.6.0",
"author": {
"name": "shennan",
"email": "shennan@syinpo.com"
},
"assets": {
"icon": "assets/icon.png"
},
"externalJS": [
"./src/cover.js"
],
"style": "style/visual.less",
"capabilities": "capabilities.json",
"dependencies": null,
"stringResources": []
}

7
src/cover.js Normal file
View File

@ -0,0 +1,7 @@
var Debug=true;
function blankConsoleDebug(){
return;
}
if(!Debug){
console.debug=blankConsoleDebug;
}

View File

@ -0,0 +1,99 @@
"Use strict";
import powerbi from "powerbi-visuals-api";
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import DataView = powerbi.DataView;
import * as visualInterfaces from "../visual/visualInterfaces";
import * as d3 from "d3";
import { IFilterManager } from "../visual/visualInterfaces";
import * as models from 'powerbi-models';
import { select, json } from "d3";
export class FilterManager implements IFilterManager{
private target: models.IFilterColumnTarget={table:'',column:''};
private host:IVisualHost;
private _jsonFilter:models.IAdvancedFilter;
public get jsonFilter() : models.IAdvancedFilter {
return this._jsonFilter;
}
public get advancedEditModeMark():string{
return 'SyinpoSlicer_AdvancedEditMode_'+this.inAdvancedEditMode;
}
constructor(host:IVisualHost){
this.host=host;
}
_inAdvancedEditMode: boolean=false;
public set inAdvancedEditMode(newValue:boolean){
this._inAdvancedEditMode=this._inAdvancedEditMode?true:newValue;
}
public get inAdvancedEditMode():boolean{
return this._inAdvancedEditMode;
}
setFilter(jsonFilter:models.IAdvancedFilter,applyNow:boolean): void {
jsonFilter.conditions[0].value=this.advancedEditModeMark
this._jsonFilter=jsonFilter;
if(applyNow){
this.applyJsonFilter();
}
}
public applyJsonFilter():void{
console.debug('filter manager applyJsonFilter start');
this.host.applyJsonFilter(this._jsonFilter,'general','filter',powerbi.FilterAction.merge);
console.debug(this.jsonFilter);
console.debug('filter manager applyJsonFilter end');
}
public dispose():void{
console.debug('filterManager disposing');
if(this.target.table&&this.target.column){
this.clear();
}
console.debug('filterManager disposed');
}
public clear():void{
console.debug("clear start:");
this.host.applyJsonFilter(null,"general","filter",powerbi.FilterAction.remove);
console.debug("clear end");
}
public update(field:powerbi.DataViewCategoryColumn):void{
let queryName:string=field.source.queryName;
let splitPosition:number=queryName.indexOf('.');
this.target.table=queryName.substring(0,splitPosition); // table
this.target.column=queryName.substring(splitPosition+1,queryName.length); // column
console.debug("queryName:",queryName);
console.debug("queryName.length:",queryName.length);
console.debug("filterManager update end, now filter:",this.target);
}
public setFilter_String(selection:string[],applyNow:boolean):void{
console.debug("setFilter_String start:",selection);
let conditions:models.IAdvancedFilterCondition[]=[];
conditions.push({
"operator":"StartsWith",
"value":this.advancedEditModeMark
});
selection.forEach(function(item){
conditions.push({
"operator":"Is",
"value":item
});
});
let jsonFilter:models.IAdvancedFilter={
"$schema": "http://powerbi.com/product/schema#advanced",
"target": this.target,
"filterType": models.FilterType.Advanced,
"conditions":conditions,
"logicalOperator":"Or"
}
console.debug("jsonFilter:",jsonFilter);
this._jsonFilter=jsonFilter;
if(applyNow){
this.applyJsonFilter();
}
console.debug("setFilter_String end");
}
}

4
src/managers/index.ts Normal file
View File

@ -0,0 +1,4 @@
export {FilterManager} from "./filterManager";
export {LayoutManager} from "./layoutManager";
export {SelectorManager} from "./selectorManager";
export {ManagerFactory} from "./managerFactory";

View File

@ -2,13 +2,33 @@
import powerbi from "powerbi-visuals-api";
import DataView = powerbi.DataView;
import * as visualInterfaces from "../visual/visualInterfaces";
import * as viewManager from "./viewManager";
export class layoutManager implements layoutManager{
layout:HTMLElement;
constructor(layout:HTMLElement){
import * as d3 from "d3";
import { ILayoutManager,IFilterManager } from "../visual/visualInterfaces";
import { IFilter } from "powerbi-models";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
export class LayoutManager implements ILayoutManager{
layout:Selection<HTMLElement>;
// constructor(layout:Selection<HTMLElement>){
// this.layout=layout;
// }
headerContainer:Selection<HTMLDivElement>
constructor(layout:Selection<HTMLElement>,filterManager:IFilterManager){
this.layout=layout;
this.headerContainer=layout.append("div").classed("header-container",true);
this.headerContainer.append('button').attr('clear',true).text('clear').on('click',function(){
//filterManager.clear();
});
this.headerContainer.style('display','none');
}
updateLayout(dataView:DataView):visualInterfaces.viewManager{
return new viewManager.dropDownViewManager();
public dispose():void{
console.debug('layoutManager disposing');
this.headerContainer.remove();
this.layout.remove();
console.debug('layoutManager disposed');
}
}
update(dataView:DataView,width:number,height:number):void{
this.layout.style("width",width+"px").style("height",height+"px");
}
}

View File

@ -0,0 +1,18 @@
"Use strict";
import * as visualInterfaces from "../visual/visualInterfaces";
import * as d3 from "d3";
import powerbi from "powerbi-visuals-api";
import { EventEmitter } from "events";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
export class ManagerFactory{
public static CreateLayoutManager(classLayoutManager:visualInterfaces.ILayoutManagerConstructor,container:Selection<HTMLElement>,filterManager:visualInterfaces.IFilterManager):visualInterfaces.ILayoutManager{
return new classLayoutManager(container,filterManager);
}
public static CreateSelectorManager(classSelectorManager:visualInterfaces.ISelectorManagerConstructor,container:Selection<HTMLElement>):visualInterfaces.ISelectorManager&EventEmitter{
return new classSelectorManager(container);
}
public static CreateFilterManager(classSelectorManager:visualInterfaces.IFilterManagerConstructor,host:powerbi.extensibility.visual.IVisualHost):visualInterfaces.IFilterManager{
return new classSelectorManager(host);
}
}

View File

@ -0,0 +1,75 @@
"Use strict";
import powerbi from "powerbi-visuals-api";
import {ISelectorManager,ISelectorManagerConstructor} from "../visual/visualInterfaces";
import * as d3 from "d3";
import * as settings from "../settings";
import * as selectors from "./selectors";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
import {EventEmitter} from "events";
/*
*/
export class SelectorManager extends EventEmitter implements ISelectorManager {
public selectorContainer : Selection<HTMLElement>;
private selector:selectors.ISelector;
private field: powerbi.DataViewCategoryColumn;
private defaultSelect: powerbi.DataViewValueColumn;
private defaultStart: powerbi.DataViewValueColumn;
private defaultEnd: powerbi.DataViewValueColumn;
//events
public stringFieldFilter_event: string="stringFieldFilter_event";
constructor(selectorContainer:Selection<HTMLElement>){
super();
console.debug("selectorManager constructor start");
this.selectorContainer=selectorContainer;
}
select(items: string[]): void {
this.selector.select(items);
}
updateFormat(visualSettings:settings.VisualSettings,width:number,height:number): void {
console.debug('selectorManager updateFormat start');
console.debug('visualSettings:',visualSettings);
if(visualSettings&&visualSettings.items){
this.selector.updateFormat(visualSettings.items,width,height);
}
console.debug('selectorManager updateFormat end');
}
public dispose():void{
this.selector.dispose();
this.selectorContainer.remove();
}
public switchSelector<T extends selectors.ISelector>(classSelector:new ()=>T){
let newSelector=new classSelector();
this.selector?.dispose();
this.selector=newSelector;
}
updateData(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn){
if(!this.selector){
let fieldType=field.source.type;
if(fieldType.text){
let dropDownSelectorContainer=this.selectorContainer.append("div").classed("dropDown-selector-container",true);
this.selector=new selectors.DropDownSelector(dropDownSelectorContainer);
}else if(fieldType.numeric){
let dropDownSelectorContainer=this.selectorContainer.append("div").classed("dropDown-selector-container",true);
this.selector=new selectors.DropDownSelector(dropDownSelectorContainer);
}else if(fieldType.dateTime){
let dropDownSelectorContainer=this.selectorContainer.append("div").classed("dropDown-selector-container",true);
this.selector=new selectors.DropDownSelector(dropDownSelectorContainer);
}else{
throw "only receive text/num/datetime field";
}
this.selector.on(this.selector.stringFieldFilter_event,(selectionValues:string[],applyNow:boolean)=>{
this.emit(this.stringFieldFilter_event,selectionValues,applyNow);
});
console.debug('selector stringFieldFilter registered');
}
this.selector.update(field, defaultSelect, defaultStart, defaultEnd);
};
}

View File

@ -0,0 +1,168 @@
import { Selector } from "./selector";
import * as settings from "../../settings";
import powerbi from "powerbi-visuals-api";
import * as d3 from "d3";
import { selection } from "d3";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
import {pixelConverter} from "powerbi-visuals-utils-typeutils";
export class DropDownSelector extends Selector {
public select(items: string[]): void {
console.debug('dropDownSelector select items:',items);
this.dropDown.selectAll('option').each(function(d,i){
console.debug('this.text:',d3.select(this).text());
if(items.includes(d3.select(this).text())){
console.debug('dropDownSelector select, included this:',this);
d3.select(this).attr('selected','selected');
}else{
console.debug('dropDownSelector select, not included this:',this);
d3.select(this).attr('selected',null);
}
});
}
public updateFormat(itemsSetting: settings.ItemsSetting, width: number, height: number) {
console.debug('dropDownSelector updateFormat start');
if (itemsSetting) {
this.dropDown.style('background-color', itemsSetting.backgroundColor)
.attr('postion','absolute')
.style('color', itemsSetting.fontColor)
.style('font-size',pixelConverter.fromPointToPixel(itemsSetting.textSize) + 'px')
.style('width', (width - 10) + 'px')
.style('border-style', settings.Enums.getOutlineStyle(itemsSetting.outline))
.style('border-width', '2px')
.style('border-color', 'black');
this.dropDown.selectAll('option').style('background-color', itemsSetting.backgroundColor)
.style('color', itemsSetting.fontColor)
.style('font-size', pixelConverter.fromPointToPixel(itemsSetting.textSize) + 'px')
.style('border-style', settings.Enums.getOutlineStyle(itemsSetting.outline))
.style('border-width', '2px')
.style('border-color', 'black');
}
console.debug('dropDownSelector updateFormat end');
}
protected checkDefaultSelectionChange(defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn): boolean {
//The defaultSelect is nullable, so first check the new defaultSelect
console.debug('checkDefaultSelectionChange start');
let flag: boolean;
if (!defaultSelect || !defaultSelect.values || !defaultSelect.values[0]) {
console.debug('new defaultSelect is null, set flag=false')
flag = flag || false;
} else {
console.debug('new defaultSelect is not null, check the oldDefaultSelect');
console.debug('new defaultSelect:', defaultSelect);
if (!this.defaultSelect || !this.defaultSelect.values || !defaultSelect.values[0]) {
console.debug('previous defaultSelect is null, set flag=true');
flag = flag || true;
} else {
if (defaultSelect.values[0].toString() != this.defaultSelect.values[0].toString()) {
console.debug('new defaultSelect not equal to the previous, set flag=true');
flag = flag || true;
} else {
console.debug('new defaultSelect equal to the previous, set flag=false');
flag = flag || false;
}
}
}
console.debug('checkDefaultSelectionChange end, result:', flag);
return flag;
}
private container: Selection<HTMLDivElement>;
private dropDown: Selection<HTMLSelectElement>;
protected createView(container: Selection<HTMLDivElement>) {
console.debug('dropDownViewManager', 'createView start');
this.container = container;
this.dropDown = this.container.append("select").classed("dropDown-selector", true).classed("selector", true);//.attr('multiple',true);
let dropDownSelector=this;
//on change, filter
this.dropDown.on("input change", function () {
console.debug('DropDownSelector:', "input change");
console.debug('this', this);
let selectedValues: string[] = [];
for (let i = 0; i < this.selectedOptions.length; i++) {
let option = this.selectedOptions.item(i);
if (option.selected) {
console.debug("option selected", option);
console.debug('option.text:', option.text);
selectedValues.push(option.text);
}
}
if (selectedValues.length == 0) {
} else {
console.debug("selection", selectedValues);
dropDownSelector.emit(dropDownSelector.stringFieldFilter_event,selectedValues,true);
}
console.debug("this", this.selectedOptions);
});
console.debug('dropDownViewManager', 'createView end');
}
public dispose() {
console.debug('dropDown-selector disposing');
this.dropDown.remove();
console.debug('dropDown-selector disposed');
}
public update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn) {
console.debug('dropDownViewManager', 'update start');
//Check field, defaultSelect are changed or not
let needUpdateDefaultSelection: boolean = this.checkFieldAndDefaultSelectionChange(field, defaultSelect, defaultStart, defaultEnd);
let newDefaultSelect: string;
if (needUpdateDefaultSelection && defaultSelect && defaultSelect.values && defaultSelect.values[0]) {
newDefaultSelect = defaultSelect.values[0].toString();
console.debug('newDefaultSelect:', newDefaultSelect);
}
//let values=field.values
let options: d3.Selection<HTMLOptionElement, powerbi.PrimitiveValue, HTMLSelectElement, any> = <d3.Selection<HTMLOptionElement, powerbi.PrimitiveValue, HTMLSelectElement, any>>this.dropDown.selectAll("option").data(field.values, function (d) { return d.toString(); });//map data
options.exit().remove();//delete
if (needUpdateDefaultSelection && newDefaultSelect) {
console.debug('reset defaultSelection start');
options = options.enter().append("option")
.text(function (d) { return d.toString(); })
.attr('label', function (d) { return d.toString(); })
.classed('dropDown-option', true)
.attr('title',function (d) { return d.toString(); })
.merge(options)
.attr("selected", function (d) {
console.debug('d.toString():', d.toString());
console.debug('newDefaultSelect:', newDefaultSelect);
return (d.toString() == newDefaultSelect) ? 'selected' : null;
});
console.debug('Set defaultSelection end');
} else {
console.debug('defaultSelection not reset, start update options');
options=options.enter().append("option")
.text(function (d) { return d.toString(); })
.attr('label', function (d) { return d.toString(); })
.attr('title',function (d) { return d.toString(); })
.attr('label', function (d) { return d.toString(); }).classed('dropDown-option', true).merge(options);//add
console.debug('defaultSelection not reset, end update options');
}
let selectedValues: string[] = [];
this.dropDown.selectAll('option').each(function(d,i){
let option=d3.select(this);
console.debug('option',option.text());
console.debug('selected?',option.property('selected'));
if(option.property('selected')){
console.debug('find a selected option:',option.text());
selectedValues.push(option.text());
}
});
if (selectedValues.length == 0) {
} else {
console.debug("selection", selectedValues);
}
this.emit(this.stringFieldFilter_event,selectedValues,false);
console.debug("dropDown:", this.dropDown);
this.field = field;
this.defaultSelect = defaultSelect;
console.debug('new defaultSelect:', defaultSelect);
console.debug('this.defaultSelect=defaultSelect, this.defaultSelect:', this.defaultSelect);
console.debug('dropDownViewManager', 'update end');
}
}

View File

@ -0,0 +1,5 @@
/*
In the future, move all selectors here.
*/
export {ISelector,Selector} from "./selector";
export {DropDownSelector} from "./dropDownSelector";

View File

View File

@ -0,0 +1,52 @@
import powerbi from "powerbi-visuals-api";
import * as settings from "../../settings";
import * as d3 from "d3";
import { EventEmitter } from "events";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
export interface ISelector extends EventEmitter{
update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn):void;
dispose():void;
select(items:string[]):void;
updateFormat(itemsSetting:settings.ItemsSetting,width:number,height:number):void;
stringFieldFilter_event:string;//filter a string field, give one param: string[]
}
export abstract class Selector extends EventEmitter implements ISelector{
protected field: powerbi.DataViewCategoryColumn;
protected defaultSelect: powerbi.DataViewValueColumn;
protected defaultStart: powerbi.DataViewValueColumn;
protected defaultEnd: powerbi.DataViewValueColumn;
//events
public stringFieldFilter_event:string="stringFieldFilter_event";
protected checkFieldChange(field: powerbi.DataViewCategoryColumn):boolean{
//Field is not null, so first check the old field
if(!this.field){
return true;
}
if(field.source.queryName==this.field.source.queryName){
console.debug('checkFieldChange: false');
return false;
}else{
console.debug('checkFieldChange: true');
return true;
}
}
protected abstract checkDefaultSelectionChange(defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn):boolean;
protected checkFieldAndDefaultSelectionChange(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn){
console.debug('checkFieldAndDefaultSelectionChange start');
let flag:boolean=this.checkFieldChange(field)||this.checkDefaultSelectionChange(defaultSelect,defaultStart,defaultEnd)
console.debug('checkFieldAndDefaultSelectionChange end, result:',flag);
return flag;
}
constructor(container:Selection<HTMLDivElement>){
super();
console.debug("Abstract selector:","constructor start");
this.createView(container);
}
public abstract select(items: string[]):void;
public abstract updateFormat(itemsSetting: settings.ItemsSetting,width:number,height:number):void;
protected abstract createView(container:Selection<HTMLDivElement>):void;
public abstract dispose():void;
public abstract update(field: powerbi.DataViewCategoryColumn, defaultSelect: powerbi.DataViewValueColumn, defaultStart: powerbi.DataViewValueColumn, defaultEnd: powerbi.DataViewValueColumn):void;
}

View File

@ -1,20 +0,0 @@
"Use strict";
import {IViewManager} from "../visual/dependencies";
export class dropDownViewManager implements IViewManager{
view: HTMLElement;
updateView(categories: Text, defaultSelect: Text) {
console.debug('dropDownViewManager','updateView')
}
}
export class listViewManager implements IViewManager{
view: HTMLElement;
updateView(categories: Text, defaultSelect: Text) {
console.debug('dropDownViewManager','updateView')
}
}
export class calendalViewManager implements IViewManager{
view: HTMLElement;
updateView(categories: Text, defaultSelect: Text) {
console.debug('dropDownViewManager','updateView')
}
}

View File

@ -0,0 +1 @@
export {Outline,getOutlineStyle} from "./outline";

View File

@ -0,0 +1,38 @@
"Use strict";
export enum Outline {
None, BottomOnly, TopOnly, LeftOnly, RightOnly, TopAndBottom, LeftAndRight, Frame
}
export function getOutlineStyle(outline: Outline): string {
let outlineStyle: string;
switch (outline) {
case Outline.None:
outlineStyle = 'none';
break;
case Outline.BottomOnly:
outlineStyle = 'none none solid none';
break;
case Outline.TopOnly:
outlineStyle = 'solid none none none';
break;
case Outline.LeftOnly:
outlineStyle = 'none none none solid';
break;
case Outline.RightOnly:
outlineStyle = 'none solid none none';
break;
case Outline.TopAndBottom:
outlineStyle = 'solid none';
break;
case Outline.LeftAndRight:
outlineStyle = 'none solid';
break;
case Outline.Frame:
outlineStyle = 'solid';
break;
default:
break;
}
return outlineStyle;
}

4
src/settings/index.ts Normal file
View File

@ -0,0 +1,4 @@
export {ItemsSetting} from "./itemsSetting";
export {SlicerHeaderSetting} from "./slicerHeaderSetting";
export {VisualSettings} from "./visualSettings";
export * as Enums from "./enums";

View File

@ -0,0 +1,8 @@
"Use strict";
import * as enums from "./enums"
export class ItemsSetting{
public fontColor:string="black";
public backgroundColor:string='transparent';
public outline:enums.Outline=enums.Outline.None;
public textSize:number=10;
}

View File

@ -0,0 +1,11 @@
"Use strict";
import * as enums from "./enums";
export class SlicerHeaderSetting{
public fontColor:string='';
public backgroundColor:string='';
public outline:enums.Outline=enums.Outline.None;
public outlineWeight:number=2;
public outlineColor:string="gray";
}

View File

@ -28,21 +28,11 @@
import { dataViewObjectsParser } from "powerbi-visuals-utils-dataviewutils";
import DataViewObjectsParser = dataViewObjectsParser.DataViewObjectsParser;
import {SlicerHeaderSetting} from "./slicerHeaderSetting";
import {ItemsSetting} from "./itemsSetting";
export class VisualSettings extends DataViewObjectsParser {
public dataPoint: dataPointSettings = new dataPointSettings();
public items: ItemsSetting = new ItemsSetting();
public slicerHeader:SlicerHeaderSetting=new SlicerHeaderSetting();
}
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;
}

View File

@ -26,7 +26,7 @@
"use strict";
import "core-js/stable";
import "./../style/visual.less";
import "../../style/visual.less";
import powerbi from "powerbi-visuals-api";
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
@ -36,38 +36,159 @@ import VisualObjectInstance = powerbi.VisualObjectInstance;
import DataView = powerbi.DataView;
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject;
import IVisualEventService = powerbi.extensibility.IVisualEventService;
import SelectionManager=powerbi.extensibility.ISelectionManager;
import * as visualInterfaces from "./visualInterfaces";
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import { VisualSettings } from "../settings/visualSettings";
import * as viewManager from "../managers/viewManager";
import { layoutManager } from "../managers/layoutManager";
import {FilterManager,ManagerFactory,SelectorManager,LayoutManager} from "../managers";
import * as d3 from "d3";
import { getObject } from "powerbi-visuals-utils-dataviewutils/lib/dataViewObjects";
import { getMeasureIndexOfRole } from "powerbi-visuals-utils-dataviewutils/lib/dataRoleHelper";
import { selector, json } from "d3";
import { debuglog } from "util";
import { EventEmitter } from "events";
import * as models from 'powerbi-models';
export class Visual implements IVisual {
/*
SelectorManager: a custom manager, used to manage dropDownSelector, listSelector, calendarSelector, and so on
SelectionManager: powerbi.extensibility
*/
private events: IVisualEventService;
private target: HTMLElement;
private settings: VisualSettings;
private layoutManager: layoutManager;
private viewManager: visualInterfaces.viewManager;
private view:SVGElement;
private selectionManager:SelectionManager;
private layoutManager: visualInterfaces.ILayoutManager;
private selectorManager: visualInterfaces.ISelectorManager&EventEmitter;
private filterManager:visualInterfaces.IFilterManager;
private readonly id:number;
private isFirstUpdate:boolean=true;
//private selectionManager:powerbi.extensibility.ISelectionManager;
constructor(options: VisualConstructorOptions) {
console.log('Visual constructor', options);
console.debug('Visual constructor start');
console.debug('options:',options);
this.events = options.host.eventService;
if (document) {
this.layoutManager=new layoutManager(options.element);
//this.selectionManager =options.host.createSelectionManager();
let container=d3.select(options.element).append("div").classed("container",true);
//overload context menu
let coverContextMenu=false;
if(coverContextMenu){
d3.select(options.element).on('contextmenu', () => {
const mouseEvent: MouseEvent = <MouseEvent>d3.event;
//const eventTarget: EventTarget = mouseEvent.target;
//let dataPoint = d3.select(eventTarget).datum();
this.selectionManager.showContextMenu({}, {
x: mouseEvent.clientX,
y: mouseEvent.clientY
});
//mouseEvent.preventDefault();
});
}
//First, create the filterManager
this.filterManager=ManagerFactory.CreateFilterManager(FilterManager,options.host);
//Then, layoutManager and selectorManager, both use the filterManager
//Lagyoutmanager manage the whole div and the slicer-header
this.layoutManager=ManagerFactory.CreateLayoutManager(LayoutManager,container,this.filterManager);
//SelectorManager manage selectors
let selectorContainer=container.append("div").classed("selector-container",true);
this.selectorManager=ManagerFactory.CreateSelectorManager(SelectorManager,selectorContainer);
//events
this.selectorManager.on(this.selectorManager.stringFieldFilter_event,(selectionValues:string[],applyNow:boolean)=>{
this.filterManager.setFilter_String(selectionValues,applyNow);
});
console.debug('selectorManager stringFieldFilter_event registered');
//set id
this.id=Math.random();
}
console.debug('end constructor');
}
public update(options: VisualUpdateOptions) {
console.debug('visual update start');
console.debug('id:',this.id);
console.debug('options:',options);
//console.debug('operationKind',options.operationKind.toString());
this.events.renderingStarted(options);
this.filterManager.inAdvancedEditMode=options.editMode==powerbi.EditMode.Advanced
this.settings = Visual.parseSettings(options && options.dataViews && options.dataViews[0]);
let viewManager:visualInterfaces.viewManager=this.layoutManager.updateLayout(options.dataViews[0]);
let dataView=options.dataViews[0];
let field=dataView.categorical.categories[0];
let messures=dataView.categorical.values;
let defaultSelect:powerbi.DataViewValueColumn;
let defaultStart:powerbi.DataViewValueColumn;
let defaultEnd:powerbi.DataViewValueColumn;
console.debug('Start map messures');
messures?.map((messure)=>{
console.debug("map");
var role=messure.source.roles;
if(role['defaultSelect']){
defaultSelect=messure;
}else if(role['defaultStart']){
defaultStart=messure;
}else if(role['defaultEnd']){
defaultEnd=messure;
};
});
let width:number=options.viewport.width;
let height:number=options.viewport.height;
console.debug("start layoutManager updateView");
this.layoutManager.update(dataView,width,height);
console.debug("start filterManager updateView");
this.filterManager.update(field);
console.debug("start selectorManager update");
this.selectorManager.updateData(field,defaultSelect,defaultStart,defaultEnd);
this.selectorManager.updateFormat(this.settings,width,height);
console.debug("previous filter",JSON.stringify(options.jsonFilters));
/*
Compare two jsonFilters(filterManager.jsonFilter and options.jsonFilters[0]).
Use method JSON.stringify.
1. filterManager.jsonFilter is null, options.jsonFilter[0] is null
Don't restore
2. filterManager.jsonFilter is Null, options.jsonFilter[0] is not null
Restore
3. filterManager.jsonFilter is not null, options.jsonFilter[0] is null
Don't restore
4. filterManager.jsonFilter != options.jsonFilter[0]
Restore
5. filterManager.jsonFilter == options.jsonFilter[0]
Don't restore
*/
if(options.jsonFilters&&options.jsonFilters[0]&&this.isFirstUpdate){
console.debug('options.jsonFilters[0] is not null');
if((!this.filterManager.jsonFilter)||
(JSON.stringify(this.filterManager.jsonFilter)!=JSON.stringify(options.jsonFilters[0]))){
console.debug('Two filter are different');
let advancedFilter:models.IAdvancedFilter=options.jsonFilters[0] as models.IAdvancedFilter;
console.debug("Cast as advancedFilter:",advancedFilter);
if(advancedFilter.conditions[0].value=='SyinpoSlicer_AdvancedEditMode_true'){
console.debug('previous filter is created in AdvancedEditMode');
}else{
//Sync slicers
console.debug('Sync slicers init or restore a bookmark start');
this.filterManager.setFilter(advancedFilter,false);
let oldSelection:string[]=[];
advancedFilter.conditions.map(function(condition,index){
oldSelection.push(condition.value as string);
});
this.selectorManager.select(oldSelection);
console.debug('Sync slicers init or restore a bookmark end');
}
}else{
console.debug('Two filter are equal');
}//end if
}else{
console.debug('Neednt init a sync slicer or restore a bookmark');
}
console.debug('visual update end');
this.isFirstUpdate=false;
this.filterManager.applyJsonFilter();
this.events.renderingFinished(options);
}
private static parseSettings(dataView: DataView): VisualSettings {
return <VisualSettings>VisualSettings.parse(dataView);
}

View File

@ -1,14 +1,57 @@
"Use strict";
import powerbi from "powerbi-visuals-api";
import DataView = powerbi.DataView;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import {VisualSettings} from "../settings/visualSettings";
export interface layoutManager{
layout:HTMLElement;
constructor(layout:HTMLElement):void;
updateLayout(dataView:DataView):viewManager;
import * as d3 from "d3";
import { IFilter } from "powerbi-models";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
import {EventEmitter} from "events";
import * as models from 'powerbi-models';
/*
Main interfaces
*/
//IVisualSettings
//ISelectorManager
export interface ILayoutManager{
layout:Selection<HTMLElement>;
update(dataView:DataView,width:number,height:number):void;
dispose():void;
}
export interface viewManager{
view:HTMLElement;
updateView(categories:any,defaultSelect:any,defaultStart:any,defaultEnd:any);
export interface ILayoutManagerConstructor{
new(container:Selection<HTMLElement>,filterManager:IFilterManager):ILayoutManager;
}
//ISelectorManager
export interface ISelectorManager{
selectorContainer:Selection<HTMLElement>;
stringFieldFilter_event:string;//filter a string field, give one param: string[]
dispose():void;
updateData(field:powerbi.DataViewCategoryColumn,defaultSelect:powerbi.DataViewValueColumn,defaultStart:powerbi.DataViewValueColumn,defaultEnd:powerbi.DataViewValueColumn):void;
updateFormat(visualSettings:VisualSettings,width:number,height:number):void;
select(items:string[]):void;
}
export interface ISelectorManagerConstructor{
new(selectorContainer:Selection<HTMLElement>):ISelectorManager&EventEmitter;
}
//IFilterManager
export interface IFilterManager{
jsonFilter:models.IAdvancedFilter;
inAdvancedEditMode:boolean;
update(field:powerbi.DataViewCategoryColumn):void;
setFilter_String(selection:string[],applyNow:boolean):void;
setFilter(jsonFilter:models.IAdvancedFilter,applyNow:boolean):void;
applyJsonFilter():void;
clear():void;
dispose():void;
}
export interface IFilterManagerConstructor{
new(host:IVisualHost):IFilterManager;
}
//interface

File diff suppressed because one or more lines are too long