* Added color customization using categorical dataView

* Changed DataView mapping from categorical to matrix.

* Fix capabilities.json and tslint issues

* Changed SourceLabels and DestinationLabels kind from grouping to measure

Co-authored-by: Luis Torres <torres.luis@microsoft.com>
Co-authored-by: Nikita Grachev <v-grniki@microsoft.com>
Co-authored-by: Ilfat Galiev <zbritva@gmail.com>
This commit is contained in:
lutorr-ms 2020-11-10 00:33:55 -08:00 коммит произвёл GitHub
Родитель 363bb2e852
Коммит d7db394c69
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 233 добавлений и 124 удалений

Просмотреть файл

@ -6,6 +6,12 @@
"displayName": "Source",
"displayNameKey": "Visual_Source"
},
{
"name": "SourceLabels",
"kind": "Measure",
"displayName": "Source labels",
"displayNameKey": "Visual_SourceLabels"
},
{
"name": "Destination",
"kind": "Grouping",
@ -14,16 +20,10 @@
},
{
"name": "DestinationLabels",
"kind": "Grouping",
"kind": "Measure",
"displayName": "Destination labels",
"displayNameKey": "Visual_DestinationLabels"
},
{
"name": "SourceLabels",
"kind": "Grouping",
"displayName": "Source labels",
"displayNameKey": "Visual_SourceLabels"
},
{
"name": "Weight",
"kind": "Measure",
@ -57,38 +57,39 @@
}
}
],
"categorical": {
"categories": {
"matrix": {
"rows": {
"select": [
{
"bind": {
"to": "Source"
"for": {
"in": "Source"
}
},
{
"bind": {
"to": "Destination"
}
},
{
"bind": {
"to": "SourceLabels"
}
},
{
"bind": {
"to": "DestinationLabels"
"for": {
"in": "Destination"
}
}
],
"dataReductionAlgorithm": {
"top": {}
}
]
},
"values": {
"for": {
"in": "Weight"
}
"select": [
{
"for": {
"in": "SourceLabels"
}
},
{
"for": {
"in": "DestinationLabels"
}
},
{
"for": {
"in": "Weight"
}
}
]
}
}
}

Просмотреть файл

@ -61,7 +61,8 @@ export interface SankeyDiagramRect {
export interface SankeyDiagramNode extends
TooltipEnabledDataPoint,
SankeyDiagramRect {
SankeyDiagramRect,
SelectableDataPoint {
label: SankeyDiagramLabel;
inputWeight: number;

Просмотреть файл

@ -51,6 +51,7 @@ import ILocalizationManager = powerbi.extensibility.ILocalizationManager;
import DataViewMetadataColumn = powerbi.DataViewMetadataColumn;
import DataViewValueColumns = powerbi.DataViewValueColumns;
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject;
import DataViewMatrixNode = powerbi.DataViewMatrixNode;
// powerbi.visuals
import ISelectionId = powerbi.visuals.ISelectionId;
@ -379,57 +380,113 @@ export class SankeyDiagram implements IVisual {
this.main.attr("transform", translate(this.margin.left, this.margin.top));
}
// tslint:disable-next-line: max-func-body-length
public converter(dataView: DataView): SankeyDiagramDataView {
const settings: SankeyDiagramSettings = this.parseSettings(dataView);
if (!dataView
|| !dataView.categorical
|| !dataView.categorical.categories
|| !dataView.categorical.categories[0]
|| !dataView.categorical.categories[1]
|| !dataView.categorical.categories[0].values
|| !dataView.categorical.categories[1].values) {
return {
settings,
nodes: [],
links: [],
columns: []
};
}
|| !dataView.matrix
|| !dataView.matrix.rows
|| !dataView.matrix.rows.levels
|| !dataView.matrix.rows.levels[0]
|| !dataView.matrix.rows.levels[0].sources
|| !dataView.matrix.rows.levels[0].sources[0]
|| !dataView.matrix.rows.levels[0].sources[0].displayName
|| !dataView.matrix.rows.levels[1]
|| !dataView.matrix.rows.levels[1].sources
|| !dataView.matrix.rows.levels[1].sources[0]
|| !dataView.matrix.rows.levels[1].sources[0].displayName
|| !dataView.matrix.rows.root
|| !dataView.matrix.rows.root.children
|| !dataView.matrix.valueSources) {
return {
settings,
nodes: [],
links: [],
columns: []
}
}
let nodes: SankeyDiagramNode[],
links: SankeyDiagramLink[],
sourceCategory: DataViewCategoryColumn = dataView.categorical.categories[0],
sourceCategories: any[] = sourceCategory.values,
destinationCategories: any[] = dataView.categorical.categories[1].values,
sourceCategoryLabels: any[] = (dataView.categorical.categories[2] || { values: [] }).values,
destinationCategoriesLabels: any[] = (dataView.categorical.categories[3] || { values: [] }).values,
categories: any[] = [],
sourceCategories: any[] = [],
destinationCategories: any[] = [],
sourceCategoriesLabels: any[] = [],
destinationCategoriesLabels: any[] = [],
objects: any[] = [],
weights: any[] = [],
selectionIdBuilder: SelectionIdBuilder = new SelectionIdBuilder(
this.visualHost,
dataView.categorical.categories);
dataView.matrix),
valueSources = dataView.matrix.valueSources,
sourceLabelIndex: number,
destinationLabelIndex: number,
weightIndex: number;
sourceLabelIndex = valueSources.indexOf(valueSources.filter((column: powerbi.DataViewMetadataColumn) => {
return column.roles.SourceLabels;
}).pop());
destinationLabelIndex = valueSources.indexOf(valueSources.filter((source: powerbi.DataViewMetadataColumn) => {
return source.roles.DestinationLabels;
}).pop());
weightIndex = valueSources.indexOf(valueSources.filter((source: powerbi.DataViewMetadataColumn) => {
return source.roles.Weight;
}).pop());
dataView.matrix.rows.root.children.forEach((source: DataViewMatrixNode) =>{
objects.push(source.objects);
});
dataView.matrix.rows.root.children.forEach((source: DataViewMatrixNode) =>{
categories.push(source.levelValues[0].value);
source.children.forEach((destination: DataViewMatrixNode) => {
sourceCategories.push(source.levelValues[0].value);
destinationCategories.push(destination.levelValues[0].value);
// If both source and destination labels are present in DataView, populate appropiate arrays
if (sourceLabelIndex != -1 && destinationLabelIndex != -1)
{
sourceCategoriesLabels.push(destination.values[sourceLabelIndex].value);
destinationCategoriesLabels.push(destination.values[destinationLabelIndex].value);
}
// If weights are present, populate the weights array
if (weightIndex != -1)
{
weights.push(destination.values[weightIndex].value);
}
objects.push(destination.objects);
categories.push(destination.levelValues[0].value);
});
});
nodes = this.createNodes(
categories,
sourceCategories,
destinationCategories,
settings,
selectionIdBuilder,
sourceCategory.source,
sourceCategory.objects || [],
sourceCategoryLabels,
dataView.matrix.rows.levels[0].sources[0],
objects,
sourceCategoriesLabels,
destinationCategoriesLabels);
links = this.createLinks(
nodes,
selectionIdBuilder,
sourceCategories,
destinationCategories,
dataView.categorical.values,
sourceCategory.objects || [],
settings,
dataView.categorical.categories[SankeyDiagram.SourceCategoryIndex].source.displayName,
dataView.categorical.categories[SankeyDiagram.DestinationCategoryIndex].source.displayName,
dataView.categorical.values ? dataView.categorical.values[SankeyDiagram.FirstValueIndex].source.displayName : null
selectionIdBuilder,
weights,
objects,
dataView.matrix.rows.levels[0].sources[0].displayName,
dataView.matrix.rows.levels[1].sources[0].displayName,
dataView.matrix.valueSources[weightIndex] ? dataView.matrix.valueSources[weightIndex].displayName : null,
dataView.matrix.valueSources
);
let cycles: SankeyDiagramCycleDictionary = this.checkCycles(nodes);
@ -643,19 +700,21 @@ export class SankeyDiagram implements IVisual {
}
private createNodes(
categories: any[],
sourceCategories: any[],
destinationCategories: any[],
settings: SankeyDiagramSettings,
selectionIdBuilder: SelectionIdBuilder,
source: DataViewMetadataColumn,
linksObjects: DataViewObjects[],
objects: DataViewObjects[],
sourceCategoriesLabels?: any[],
destinationCategoriesLabels?: any[]): SankeyDiagramNode[] {
let nodes: SankeyDiagramNode[] = [],
valueFormatterForCategories: IValueFormatter,
nodeFillColor: string,
nodeStrokeColor: string;
nodeStrokeColor: string,
selectionId: ISelectionId;
valueFormatterForCategories = valueFormatter.create({
format: valueFormatter.getFormatStringByColumn(source),
@ -681,7 +740,6 @@ export class SankeyDiagram implements IVisual {
labelsDictionary[item] = destinationCategoriesLabels[index] || "";
});
let categories: any[] = sourceCategories.concat(destinationCategories);
categories.forEach((item: any, index: number) => {
let formattedValue: string = valueFormatterForCategories.format((<string>labelsDictionary[item].toString()).replace(SankeyDiagram.DuplicatedNamePostfix, "")),
label: SankeyDiagramLabel,
@ -700,9 +758,14 @@ export class SankeyDiagram implements IVisual {
height: textMeasurementService.estimateSvgTextHeight(textProperties),
color: settings.labels.fill
};
nodeFillColor = this.colorHelper.isHighContrast ? this.colorHelper.getThemeColor() : this.colorPalette.getColor(item).value;
nodeFillColor = this.getColor(
SankeyDiagram.NodesPropertyIdentifier,
this.colorPalette.getColor(item).value,
objects[index]);
nodeStrokeColor = this.colorHelper.getHighContrastColor("foreground", nodeFillColor);
selectionId = selectionIdBuilder.createSelectionId(index);
if (nodes.filter((node: SankeyDiagramNode) => {
return node.label.name === item;
}).length === 0)
@ -720,51 +783,43 @@ export class SankeyDiagram implements IVisual {
strokeColor: nodeStrokeColor,
tooltipInfo: [],
selectableDataPoints: [],
settings: null
settings: null,
identity: selectionId,
selected: false
});
});
return nodes;
}
// tslint:disable-next-line: max-func-body-length
private createLinks(
nodes: SankeyDiagramNode[],
selectionIdBuilder: SelectionIdBuilder,
sourceCategories: any[],
destinationCategories: any[],
valueColumns: DataViewValueColumns,
linksObjects: DataViewObjects[],
settings: SankeyDiagramSettings,
selectionIdBuilder: SelectionIdBuilder,
weights: any[],
objects: DataViewObjects[],
sourceFieldName: string,
destinationFieldName: string,
valueFieldName: string
valueFieldName: string,
valueSources: any
): SankeyDiagramLink[] {
let valuesColumn: DataViewValueColumn = valueColumns && valueColumns[0],
links: SankeyDiagramLink[] = [],
let links: SankeyDiagramLink[] = [],
weightValues: number[] = [],
dataPoints: SankeyDiagramDataPoint[] = [],
valuesFormatterForWeigth: IValueFormatter,
formatOfWeigth: string = SankeyDiagram.DefaultFormatOfWeigth;
if (valuesColumn && valuesColumn.values && valuesColumn.values.map) {
weightValues = valuesColumn.values.map((value: any) => {
return value
? value
: SankeyDiagram.DefaultWeightValue;
});
}
if (valuesColumn && valuesColumn.source) {
formatOfWeigth = valueFormatter.getFormatStringByColumn(valuesColumn.source);
}
formatOfWeigth = valueFormatter.getFormatStringByColumn(valueSources);
dataPoints = sourceCategories.map((item: any, index: number) => {
return {
source: item,
destination: destinationCategories[index],
weigth: valuesColumn
? weightValues[index] || SankeyDiagram.DefaultWeightValue
weigth: weights[index]
? weights[index] || SankeyDiagram.DefaultWeightValue
: SankeyDiagram.MinWeightValue
};
});
@ -797,7 +852,7 @@ export class SankeyDiagram implements IVisual {
linkFillColor = this.getColor(
SankeyDiagram.LinksPropertyIdentifier,
SankeyDiagram.DefaultColourOfLink,
linksObjects[index]);
objects[index]);
linkStrokeColor = this.colorHelper.isHighContrast ? this.colorHelper.getHighContrastColor("foreground", linkFillColor) : linkFillColor;
selectionId = selectionIdBuilder.createSelectionId(index);
@ -2346,6 +2401,10 @@ export class SankeyDiagram implements IVisual {
this.enumerateLinks(instanceEnumeration);
}
if(options.objectName === SankeyDiagram.NodesPropertyIdentifier.objectName) {
this.enumerateNodeCategories(instanceEnumeration);
}
// hide scale settings
if (options.objectName === SankeyDiagram.NodeComplexSettingsPropertyIdentifier.objectName) {
(<VisualObjectInstanceEnumerationObject>instanceEnumeration).instances = (<VisualObjectInstanceEnumerationObject>instanceEnumeration).instances
@ -2355,6 +2414,29 @@ export class SankeyDiagram implements IVisual {
return instanceEnumeration || [];
}
private enumerateNodeCategories(instanceEnumeration: VisualObjectInstanceEnumeration): void {
const nodes: SankeyDiagramNode[] = this.dataView && this.dataView.nodes;
if (!nodes || !(nodes.length > 0)) {
return;
}
nodes.filter((node: SankeyDiagramNode) => {
return !node.label.name.endsWith(SankeyDiagram.DuplicatedNamePostfix);
}).forEach((node: SankeyDiagramNode) => {
const identity: ISelectionId = <ISelectionId>node.identity,
displayName: string = node.label.formattedName;
this.addAnInstanceToEnumeration(instanceEnumeration, {
displayName,
objectName: SankeyDiagram.NodesPropertyIdentifier.objectName,
selector: identity.getSelector(),
properties: {
fill: { solid: { color: node.fillColor } }
}
});
});
}
private enumerateLinks(instanceEnumeration: VisualObjectInstanceEnumeration): void {
const links: SankeyDiagramLink[] = this.dataView && this.dataView.links;

Просмотреть файл

@ -28,56 +28,77 @@
import powerbi from "powerbi-visuals-api";
import ISelectionId = powerbi.visuals.ISelectionId;
import DataViewCategoryColumn = powerbi.DataViewCategoryColumn;
import DataViewMatrix = powerbi.DataViewMatrix;
import DataViewMatrixNode = powerbi.DataViewMatrixNode;
// powerbi.extensibility.visual
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
interface CategoryIdentityIndex {
categoryIndex: number;
identityIndex: number;
}
export class SelectionIdBuilder {
private static DefaultCategoryIndex: number = 0;
private visualHost: IVisualHost;
private categories: DataViewCategoryColumn[];
private matrix: DataViewMatrix;
constructor(
IVisualHost: IVisualHost,
categories: DataViewCategoryColumn[]) {
matrix: DataViewMatrix) {
this.visualHost = IVisualHost;
this.categories = categories || [];
}
private getIdentityById(index: number): CategoryIdentityIndex {
let categoryIndex: number = SelectionIdBuilder.DefaultCategoryIndex,
identityIndex: number = index;
for (let length: number = this.categories.length; categoryIndex < length; categoryIndex++) {
const amountOfIdentities: number = this.categories[categoryIndex].identity.length;
if (identityIndex > amountOfIdentities - 1) {
identityIndex -= amountOfIdentities;
} else {
break;
}
}
return {
categoryIndex,
identityIndex
};
this.matrix = matrix
}
public createSelectionId(index: number): ISelectionId {
const categoryIdentityIndex: CategoryIdentityIndex = this.getIdentityById(index);
let counter: number = 0,
selectionId:ISelectionId;
return this.visualHost.createSelectionIdBuilder()
.withCategory(
this.categories[categoryIdentityIndex.categoryIndex],
categoryIdentityIndex.identityIndex)
.createSelectionId();
this.matrix.rows.root.children.forEach((source: DataViewMatrixNode) => {
if (counter == index){
const categoryColumn: DataViewCategoryColumn = {
source: {
displayName: null,
// tslint:disable-next-line: insecure-random
queryName: `${Math.random()}-${+(new Date())}`
},
values: null,
identity: [source.identity]
};
selectionId = this.visualHost.createSelectionIdBuilder()
.withCategory(categoryColumn,0)
.createSelectionId();
}
counter+=1
});
this.matrix.rows.root.children.forEach((source: powerbi.DataViewMatrixNode) =>{
source.children.forEach((destination: powerbi.DataViewMatrixNode) => {
if (counter == index){
const categoryColumn1: DataViewCategoryColumn = {
source: {
displayName: null,
// tslint:disable-next-line: insecure-random
queryName: `${Math.random()}-${+(new Date())}`
},
values: null,
identity: [source.identity]
};
const categoryColumn2: DataViewCategoryColumn = {
source: {
displayName: null,
// tslint:disable-next-line: insecure-random
queryName: `${Math.random()}-${+(new Date())}`
},
values: null,
identity: [destination.identity]
};
selectionId = this.visualHost.createSelectionIdBuilder()
.withCategory(categoryColumn1,0)
.withCategory(categoryColumn2, 0)
.createSelectionId();
}
counter += 1
});
});
return selectionId;
}
}

Просмотреть файл

@ -161,7 +161,9 @@ describe("SankeyDiagram", () => {
height: 0,
colour: "",
selectionIds: [],
tooltipData: []
tooltipData: [],
identity: null,
selected: false
};
});
}
@ -206,7 +208,9 @@ describe("SankeyDiagram", () => {
height: 0,
colour: "",
selectionIds: [],
tooltipData: []
tooltipData: [],
identity: null,
selected: false
};
});
}