diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e5553..1ce5eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,23 @@ ### New features * Added "Invert Color Scale" toggle to reverse the color gradient direction +* Diverging (three-stop) gradient: new "Add gradient middle" toggle and "Gradient middle" colour picker in the Format pane → General → Gradient Colors group. When enabled, the colour scale interpolates smoothly through the chosen midpoint colour (default: `#767676`). The midpoint uses this default until the user explicitly changes it in the Format pane. + +### Bug fixes +* Fixed "Invert Color Scale" and gradient middle colour not being neutralized in high-contrast mode; both features are now automatically disabled when the Power BI high-contrast theme is active to preserve accessibility contrast requirements. +* Fixed bucket count upper bound not being restored when switching from a Colorbrewer palette back to the custom gradient mode; the maximum was previously stuck at the palette's supported range rather than resetting to 18. +* Fixed gradient middle anchor being skewed left-of-centre for even bucket counts; the three-stop domain now uses a fractional midpoint so both odd and even counts produce a symmetric diverging scale. + +### Code quality +* Renamed internal constant `AdditionalSpaceForColorbrewerCells` → `GridHeightAdjustmentFactor` to reflect that the padding applies in all rendering modes. +* `GeneralSettings.stroke` converted from a `static` mutable field to an instance field on `GeneralSettings`; high-contrast and non-high-contrast paths now reset it on every render, eliminating cross-render state leakage. +* `SettingsModel.cards` type widened from `FormattingSettingsSimpleCard[]` to `FormattingSettingsCard[]` (`SimpleCard | CompositeCard`) to correctly reflect that `GeneralSettings` extends `CompositeCard`. +* Replaced tautological `expect(querySelectorAll(…)).toBeTruthy()` assertions in unit tests with `expect(…length).toBeGreaterThan(0)`. ### Other * Upgraded powerbi-visuals-tools from ^6.1.1 to ^7.0.3 * Added unit tests for invertColorScale and getOpacity utility +* Format pane General card restructured into three named groups: **Colorbrewer**, **Gradient Colors**, and **Additional settings**. ## 4.0.0.0 diff --git a/capabilities.json b/capabilities.json index ca81a9a..a438564 100644 --- a/capabilities.json +++ b/capabilities.json @@ -227,6 +227,20 @@ } } }, + "activateGradientMiddle": { + "type": { + "bool": true + } + }, + "gradientMiddle": { + "type": { + "fill": { + "solid": { + "color": true + } + } + } + }, "gradientEnd": { "type": { "fill": { diff --git a/src/heatmapUtils.ts b/src/heatmapUtils.ts index 409d605..3182c5d 100644 --- a/src/heatmapUtils.ts +++ b/src/heatmapUtils.ts @@ -23,6 +23,18 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +import powerbi from "powerbi-visuals-api"; + +import { textMeasurementService } from "powerbi-visuals-utils-formattingutils"; + +import { pixelConverter as PixelConverter } from "powerbi-visuals-utils-typeutils"; +import { ColorHelper } from "powerbi-visuals-utils-colorutils"; + +import maxBy from "lodash.maxby"; + +import { IColorArray, TableHeatMapChartData } from "./dataInterfaces"; +import { BaseLabelCardSettings, colorbrewer, SettingsModel, YAxisLabelsSettings } from "./settings"; + export const DimmedOpacity: number = 0.4; export const DefaultOpacity: number = 1.0; export const DimmedColor: string = "black"; @@ -38,4 +50,135 @@ export function getOpacity( } return DefaultOpacity; +} + +export const YAxisAdditionalMargin: number = 5; +export const GridHeightAdjustmentFactor: number = 2; +export const ConstGridMinHeight: number = 5; +export const ConstGridMinWidth: number = 1; +export const CellMaxHeightLimit: number = 300; +export const CellMaxWidthFactorLimit: number = 15; + +export function isDataViewValid(dataView: powerbi.DataView): boolean { + return !!(dataView.categorical?.categories && dataView.categorical?.values); +} + +export function textLimit(text: string, limit: number): string { + if (text.length > limit) { + return ((text || "").substring(0, limit).trim()) + "\u2026"; + } + + return text; +} + +export function getYAxisWidth(chartData: TableHeatMapChartData, settings: YAxisLabelsSettings): number { + let maxLengthText: powerbi.PrimitiveValue = maxBy(chartData.categoryY, (d) => String(d).length) || ""; + + maxLengthText = textLimit(maxLengthText.toString(), settings.maxTextSymbol.value); + + return settings.show.value ? textMeasurementService.measureSvgTextWidth({ + fontSize: PixelConverter.toString(settings.fontSize.value), + text: maxLengthText.trim(), + fontFamily: settings.fontFamily.value.toString() + }) + YAxisAdditionalMargin : 0; +} + +export function getXAxisHeight(chartData: TableHeatMapChartData, settings: BaseLabelCardSettings): number { + const categoryX: string[] = chartData.categoryX.map(x => x?.toString() ?? ""); + const maxLengthText: powerbi.PrimitiveValue = maxBy(categoryX, "length") || ""; + + return settings.show.value ? textMeasurementService.measureSvgTextHeight({ + fontSize: PixelConverter.toString(settings.fontSize.value), + text: maxLengthText.toString().trim(), + fontFamily: settings.fontFamily.value.toString() + }) : 0; +} + +export function getYAxisHeight(chartData: TableHeatMapChartData, settings: YAxisLabelsSettings): number { + const maxLengthText: powerbi.PrimitiveValue = maxBy(chartData.categoryY, (d) => String(d).length) || ""; + + return textMeasurementService.measureSvgTextHeight({ + fontSize: PixelConverter.toString(settings.fontSize.value), + text: maxLengthText.toString().trim(), + fontFamily: settings.fontFamily.value.toString() + }); +} + +export function calculateGridSizeHeight( + viewportHeight: number, + xAxisHeight: number, + categoryYLength: number, + marginTop: number, + marginBottom: number +): number { + const gridSizeHeight: number = Math.floor( + (viewportHeight - marginTop - xAxisHeight - marginBottom - YAxisAdditionalMargin) / + (categoryYLength + GridHeightAdjustmentFactor) + ); + + return Math.max(ConstGridMinHeight, Math.min(gridSizeHeight, CellMaxHeightLimit)); +} + +export function calculateGridSizeWidth( + viewportWidth: number, + yAxisWidth: number, + categoryXLength: number, + gridSizeHeight: number +): number { + if (categoryXLength <= 0) { + return ConstGridMinWidth; + } + const gridSizeWidth: number = Math.floor((viewportWidth - yAxisWidth) / categoryXLength); + + return Math.max(ConstGridMinWidth, Math.min(gridSizeWidth, gridSizeHeight * CellMaxWidthFactorLimit)); +} + +/** + * Returns the start and end colours for the active colour source (colorbrewer palette or + * user-defined gradient). Called by `initColors` to resolve the two anchor colours before + * building a two- or three-stop scale. + */ +export function resolveStartEndColors( + colorbrewerEnable: boolean, + colorbrewerScale: string, + numBuckets: number, + gradientStart: string, + gradientEnd: string +): { startColor: string; endColor: string } { + if (colorbrewerEnable) { + const palette: IColorArray = colorbrewer[colorbrewerScale] || colorbrewer.Reds; + const colors: string[] | undefined = palette[numBuckets] ?? colorbrewer.Reds[numBuckets]; + if (!colors || colors.length === 0) { + // numBuckets is outside the supported range for all palettes; + // fall back to the user gradient endpoints so we never dereference undefined. + return { startColor: gradientStart, endColor: gradientEnd }; + } + return { startColor: colors[0], endColor: colors[colors.length - 1] }; + } + return { startColor: gradientStart, endColor: gradientEnd }; +} + +export function parseSettings(colorHelper: ColorHelper, settingsModel: SettingsModel): SettingsModel { + if (colorHelper.isHighContrast) { + const foregroundColor: string = colorHelper.getThemeColor("foreground"); + const backgroundColor: string = colorHelper.getThemeColor("background"); + + settingsModel.labels.show.value = true; + settingsModel.labels.fill.value.value = foregroundColor; + + settingsModel.xAxisLabels.fill.value.value = foregroundColor; + settingsModel.yAxisLabels.fill.value.value = foregroundColor; + + settingsModel.general.enableColorbrewer.value = false; + settingsModel.general.activateGradientMiddle.value = false; + settingsModel.general.gradientStart.value.value = backgroundColor; + settingsModel.general.gradientEnd.value.value = backgroundColor; + settingsModel.general.stroke = foregroundColor; + settingsModel.general.textColor = foregroundColor; + } else { + settingsModel.general.stroke = "#E6E6E6"; + settingsModel.general.textColor = "#AAAAAA"; + } + + return settingsModel; } \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts index a78b4b6..031ad0b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,599 +1,638 @@ -/* - * Power BI Visualizations - * - * 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. - */ -import { formattingSettings } from "powerbi-visuals-utils-formattingmodel"; - -import FormattingSettingsSimpleCard = formattingSettings.SimpleCard; -import FormattingSettingsSlice = formattingSettings.Slice; -import FormattingSettingsModel = formattingSettings.Model; - -import { IColorArray, IColorBrewer } from "./dataInterfaces"; -import powerbi from "powerbi-visuals-api"; - -export const colorbrewer: IColorBrewer = { - YlGn: { - 3: ["#f7fcb9", "#addd8e", "#31a354"], - 4: ["#ffffcc", "#c2e699", "#78c679", "#238443"], - 5: ["#ffffcc", "#c2e699", "#78c679", "#31a354", "#006837"], - 6: ["#ffffcc", "#d9f0a3", "#addd8e", "#78c679", "#31a354", "#006837"], - 7: ["#ffffcc", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#005a32"], - 8: ["#ffffe5", "#f7fcb9", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#005a32"], - 9: ["#ffffe5", "#f7fcb9", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#006837", "#004529"] - }, - YlGnBu: { - 3: ["#edf8b1", "#7fcdbb", "#2c7fb8"], - 4: ["#ffffcc", "#a1dab4", "#41b6c4", "#225ea8"], - 5: ["#ffffcc", "#a1dab4", "#41b6c4", "#2c7fb8", "#253494"], - 6: ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#2c7fb8", "#253494"], - 7: ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"], - 8: ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"], - 9: ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#253494", "#081d58"] - }, - GnBu: { - 3: ["#e0f3db", "#a8ddb5", "#43a2ca"], - 4: ["#f0f9e8", "#bae4bc", "#7bccc4", "#2b8cbe"], - 5: ["#f0f9e8", "#bae4bc", "#7bccc4", "#43a2ca", "#0868ac"], - 6: ["#f0f9e8", "#ccebc5", "#a8ddb5", "#7bccc4", "#43a2ca", "#0868ac"], - 7: ["#f0f9e8", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#08589e"], - 8: ["#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#08589e"], - 9: ["#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081"] - }, - BuGn: { - 3: ["#e5f5f9", "#99d8c9", "#2ca25f"], - 4: ["#edf8fb", "#b2e2e2", "#66c2a4", "#238b45"], - 5: ["#edf8fb", "#b2e2e2", "#66c2a4", "#2ca25f", "#006d2c"], - 6: ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#2ca25f", "#006d2c"], - 7: ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"], - 8: ["#f7fcfd", "#e5f5f9", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"], - 9: ["#f7fcfd", "#e5f5f9", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#006d2c", "#00441b"] - }, - PuBuGn: { - 3: ["#ece2f0", "#a6bddb", "#1c9099"], - 4: ["#f6eff7", "#bdc9e1", "#67a9cf", "#02818a"], - 5: ["#f6eff7", "#bdc9e1", "#67a9cf", "#1c9099", "#016c59"], - 6: ["#f6eff7", "#d0d1e6", "#a6bddb", "#67a9cf", "#1c9099", "#016c59"], - 7: ["#f6eff7", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"], - 8: ["#fff7fb", "#ece2f0", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"], - 9: ["#fff7fb", "#ece2f0", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016c59", "#014636"] - }, - PuBu: { - 3: ["#ece7f2", "#a6bddb", "#2b8cbe"], - 4: ["#f1eef6", "#bdc9e1", "#74a9cf", "#0570b0"], - 5: ["#f1eef6", "#bdc9e1", "#74a9cf", "#2b8cbe", "#045a8d"], - 6: ["#f1eef6", "#d0d1e6", "#a6bddb", "#74a9cf", "#2b8cbe", "#045a8d"], - 7: ["#f1eef6", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#034e7b"], - 8: ["#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#034e7b"], - 9: ["#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#045a8d", "#023858"] - }, - BuPu: { - 3: ["#e0ecf4", "#9ebcda", "#8856a7"], - 4: ["#edf8fb", "#b3cde3", "#8c96c6", "#88419d"], - 5: ["#edf8fb", "#b3cde3", "#8c96c6", "#8856a7", "#810f7c"], - 6: ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8856a7", "#810f7c"], - 7: ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"], - 8: ["#f7fcfd", "#e0ecf4", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"], - 9: ["#f7fcfd", "#e0ecf4", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#810f7c", "#4d004b"] - }, - RdPu: { - 3: ["#fde0dd", "#fa9fb5", "#c51b8a"], - 4: ["#feebe2", "#fbb4b9", "#f768a1", "#ae017e"], - 5: ["#feebe2", "#fbb4b9", "#f768a1", "#c51b8a", "#7a0177"], - 6: ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#c51b8a", "#7a0177"], - 7: ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177"], - 8: ["#fff7f3", "#fde0dd", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177"], - 9: ["#fff7f3", "#fde0dd", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177", "#49006a"] - }, - PuRd: { - 3: ["#e7e1ef", "#c994c7", "#dd1c77"], - 4: ["#f1eef6", "#d7b5d8", "#df65b0", "#ce1256"], - 5: ["#f1eef6", "#d7b5d8", "#df65b0", "#dd1c77", "#980043"], - 6: ["#f1eef6", "#d4b9da", "#c994c7", "#df65b0", "#dd1c77", "#980043"], - 7: ["#f1eef6", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"], - 8: ["#f7f4f9", "#e7e1ef", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"], - 9: ["#f7f4f9", "#e7e1ef", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#980043", "#67001f"] - }, - OrRd: { - 3: ["#fee8c8", "#fdbb84", "#e34a33"], - 4: ["#fef0d9", "#fdcc8a", "#fc8d59", "#d7301f"], - 5: ["#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33", "#b30000"], - 6: ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#e34a33", "#b30000"], - 7: ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"], - 8: ["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"], - 9: ["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"] - }, - YlOrRd: { - 3: ["#ffeda0", "#feb24c", "#f03b20"], - 4: ["#ffffb2", "#fecc5c", "#fd8d3c", "#e31a1c"], - 5: ["#ffffb2", "#fecc5c", "#fd8d3c", "#f03b20", "#bd0026"], - 6: ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#f03b20", "#bd0026"], - 7: ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"], - 8: ["#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"], - 9: ["#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#bd0026", "#800026"] - }, - YlOrBr: { - 3: ["#fff7bc", "#fec44f", "#d95f0e"], - 4: ["#ffffd4", "#fed98e", "#fe9929", "#cc4c02"], - 5: ["#ffffd4", "#fed98e", "#fe9929", "#d95f0e", "#993404"], - 6: ["#ffffd4", "#fee391", "#fec44f", "#fe9929", "#d95f0e", "#993404"], - 7: ["#ffffd4", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#8c2d04"], - 8: ["#ffffe5", "#fff7bc", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#8c2d04"], - 9: ["#ffffe5", "#fff7bc", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#993404", "#662506"] - }, - Purples: { - 3: ["#efedf5", "#bcbddc", "#756bb1"], - 4: ["#f2f0f7", "#cbc9e2", "#9e9ac8", "#6a51a3"], - 5: ["#f2f0f7", "#cbc9e2", "#9e9ac8", "#756bb1", "#54278f"], - 6: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"], - 7: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#4a1486"], - 8: ["#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#4a1486"], - 9: ["#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#54278f", "#3f007d"] - }, - Blues: { - 3: ["#deebf7", "#9ecae1", "#3182bd"], - 4: ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"], - 5: ["#eff3ff", "#bdd7e7", "#6baed6", "#3182bd", "#08519c"], - 6: ["#eff3ff", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"], - 7: ["#eff3ff", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"], - 8: ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"], - 9: ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#08519c", "#08306b"] - }, - Greens: { - 3: ["#e5f5e0", "#a1d99b", "#31a354"], - 4: ["#edf8e9", "#bae4b3", "#74c476", "#238b45"], - 5: ["#edf8e9", "#bae4b3", "#74c476", "#31a354", "#006d2c"], - 6: ["#edf8e9", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"], - 7: ["#edf8e9", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#005a32"], - 8: ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#005a32"], - 9: ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#006d2c", "#00441b"] - }, - Oranges: { - 3: ["#fee6ce", "#fdae6b", "#e6550d"], - 4: ["#feedde", "#fdbe85", "#fd8d3c", "#d94701"], - 5: ["#feedde", "#fdbe85", "#fd8d3c", "#e6550d", "#a63603"], - 6: ["#feedde", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"], - 7: ["#feedde", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#8c2d04"], - 8: ["#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#8c2d04"], - 9: ["#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#a63603", "#7f2704"] - }, - Reds: { - 3: ["#fee0d2", "#fc9272", "#de2d26"], - 4: ["#fee5d9", "#fcae91", "#fb6a4a", "#cb181d"], - 5: ["#fee5d9", "#fcae91", "#fb6a4a", "#de2d26", "#a50f15"], - 6: ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#de2d26", "#a50f15"], - 7: ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"], - 8: ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"], - 9: ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#a50f15", "#67000d"] - }, - Greys: { - 3: ["#f0f0f0", "#bdbdbd", "#636363"], - 4: ["#f7f7f7", "#cccccc", "#969696", "#525252"], - 5: ["#f7f7f7", "#cccccc", "#969696", "#636363", "#252525"], - 6: ["#f7f7f7", "#d9d9d9", "#bdbdbd", "#969696", "#636363", "#252525"], - 7: ["#f7f7f7", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525"], - 8: ["#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525"], - 9: ["#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525", "#000000"] - }, - PuOr: { - 3: ["#f1a340", "#f7f7f7", "#998ec3"], - 4: ["#e66101", "#fdb863", "#b2abd2", "#5e3c99"], - 5: ["#e66101", "#fdb863", "#f7f7f7", "#b2abd2", "#5e3c99"], - 6: ["#b35806", "#f1a340", "#fee0b6", "#d8daeb", "#998ec3", "#542788"], - 7: ["#b35806", "#f1a340", "#fee0b6", "#f7f7f7", "#d8daeb", "#998ec3", "#542788"], - 8: ["#b35806", "#e08214", "#fdb863", "#fee0b6", "#d8daeb", "#b2abd2", "#8073ac", "#542788"], - 9: ["#b35806", "#e08214", "#fdb863", "#fee0b6", "#f7f7f7", "#d8daeb", "#b2abd2", "#8073ac", "#542788"], - 10: ["#7f3b08", "#b35806", "#e08214", "#fdb863", "#fee0b6", "#d8daeb", "#b2abd2", "#8073ac", "#542788", "#2d004b"], - 11: ["#7f3b08", "#b35806", "#e08214", "#fdb863", "#fee0b6", "#f7f7f7", "#d8daeb", "#b2abd2", "#8073ac", "#542788", "#2d004b"] - }, - BrBG: { - 3: ["#d8b365", "#f5f5f5", "#5ab4ac"], - 4: ["#a6611a", "#dfc27d", "#80cdc1", "#018571"], - 5: ["#a6611a", "#dfc27d", "#f5f5f5", "#80cdc1", "#018571"], - 6: ["#8c510a", "#d8b365", "#f6e8c3", "#c7eae5", "#5ab4ac", "#01665e"], - 7: ["#8c510a", "#d8b365", "#f6e8c3", "#f5f5f5", "#c7eae5", "#5ab4ac", "#01665e"], - 8: ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#c7eae5", "#80cdc1", "#35978f", "#01665e"], - 9: ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e"], - 10: ["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"], - 11: ["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"] - }, - PRGn: { - 3: ["#af8dc3", "#f7f7f7", "#7fbf7b"], - 4: ["#7b3294", "#c2a5cf", "#a6dba0", "#008837"], - 5: ["#7b3294", "#c2a5cf", "#f7f7f7", "#a6dba0", "#008837"], - 6: ["#762a83", "#af8dc3", "#e7d4e8", "#d9f0d3", "#7fbf7b", "#1b7837"], - 7: ["#762a83", "#af8dc3", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#7fbf7b", "#1b7837"], - 8: ["#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837"], - 9: ["#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837"], - 10: ["#40004b", "#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837", "#00441b"], - 11: ["#40004b", "#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837", "#00441b"] - }, - PiYG: { - 3: ["#e9a3c9", "#f7f7f7", "#a1d76a"], - 4: ["#d01c8b", "#f1b6da", "#b8e186", "#4dac26"], - 5: ["#d01c8b", "#f1b6da", "#f7f7f7", "#b8e186", "#4dac26"], - 6: ["#c51b7d", "#e9a3c9", "#fde0ef", "#e6f5d0", "#a1d76a", "#4d9221"], - 7: ["#c51b7d", "#e9a3c9", "#fde0ef", "#f7f7f7", "#e6f5d0", "#a1d76a", "#4d9221"], - 8: ["#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221"], - 9: ["#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221"], - 10: ["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"], - 11: ["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"] - }, - RdBu: { - 3: ["#ef8a62", "#f7f7f7", "#67a9cf"], - 4: ["#ca0020", "#f4a582", "#92c5de", "#0571b0"], - 5: ["#ca0020", "#f4a582", "#f7f7f7", "#92c5de", "#0571b0"], - 6: ["#b2182b", "#ef8a62", "#fddbc7", "#d1e5f0", "#67a9cf", "#2166ac"], - 7: ["#b2182b", "#ef8a62", "#fddbc7", "#f7f7f7", "#d1e5f0", "#67a9cf", "#2166ac"], - 8: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac"], - 9: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac"], - 10: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"], - 11: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"] - }, - RdGy: { - 3: ["#ef8a62", "#ffffff", "#999999"], - 4: ["#ca0020", "#f4a582", "#bababa", "#404040"], - 5: ["#ca0020", "#f4a582", "#ffffff", "#bababa", "#404040"], - 6: ["#b2182b", "#ef8a62", "#fddbc7", "#e0e0e0", "#999999", "#4d4d4d"], - 7: ["#b2182b", "#ef8a62", "#fddbc7", "#ffffff", "#e0e0e0", "#999999", "#4d4d4d"], - 8: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#e0e0e0", "#bababa", "#878787", "#4d4d4d"], - 9: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#ffffff", "#e0e0e0", "#bababa", "#878787", "#4d4d4d"], - 10: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#e0e0e0", "#bababa", "#878787", "#4d4d4d", "#1a1a1a"], - 11: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#ffffff", "#e0e0e0", "#bababa", "#878787", "#4d4d4d", "#1a1a1a"] - }, - RdYlBu: { - 3: ["#fc8d59", "#ffffbf", "#91bfdb"], - 4: ["#d7191c", "#fdae61", "#abd9e9", "#2c7bb6"], - 5: ["#d7191c", "#fdae61", "#ffffbf", "#abd9e9", "#2c7bb6"], - 6: ["#d73027", "#fc8d59", "#fee090", "#e0f3f8", "#91bfdb", "#4575b4"], - 7: ["#d73027", "#fc8d59", "#fee090", "#ffffbf", "#e0f3f8", "#91bfdb", "#4575b4"], - 8: ["#d73027", "#f46d43", "#fdae61", "#fee090", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"], - 9: ["#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"], - 10: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee090", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"], - 11: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"] - }, - Spectral: { - 3: ["#fc8d59", "#ffffbf", "#99d594"], - 4: ["#d7191c", "#fdae61", "#abdda4", "#2b83ba"], - 5: ["#d7191c", "#fdae61", "#ffffbf", "#abdda4", "#2b83ba"], - 6: ["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#99d594", "#3288bd"], - 7: ["#d53e4f", "#fc8d59", "#fee08b", "#ffffbf", "#e6f598", "#99d594", "#3288bd"], - 8: ["#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#e6f598", "#abdda4", "#66c2a5", "#3288bd"], - 9: ["#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd"], - 10: ["#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"], - 11: ["#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"] - }, - RdYlGn: { - 3: ["#fc8d59", "#ffffbf", "#91cf60"], - 4: ["#d7191c", "#fdae61", "#a6d96a", "#1a9641"], - 5: ["#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641"], - 6: ["#d73027", "#fc8d59", "#fee08b", "#d9ef8b", "#91cf60", "#1a9850"], - 7: ["#d73027", "#fc8d59", "#fee08b", "#ffffbf", "#d9ef8b", "#91cf60", "#1a9850"], - 8: ["#d73027", "#f46d43", "#fdae61", "#fee08b", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850"], - 9: ["#d73027", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850"], - 10: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"], - 11: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"] - }, - Accent: { - 3: ["#7fc97f", "#beaed4", "#fdc086"], - 4: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99"], - 5: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0"], - 6: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f"], - 7: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17"], - 8: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17", "#666666"] - }, - Dark2: { - 3: ["#1b9e77", "#d95f02", "#7570b3"], - 4: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a"], - 5: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e"], - 6: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02"], - 7: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d"], - 8: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d", "#666666"] - }, - Paired: { - 3: ["#a6cee3", "#1f78b4", "#b2df8a"], - 4: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"], - 5: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99"], - 6: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c"], - 7: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f"], - 8: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00"], - 9: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6"], - 10: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a"], - 11: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99"], - 12: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99", "#b15928"] - }, - Pastel1: { - 3: ["#fbb4ae", "#b3cde3", "#ccebc5"], - 4: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4"], - 5: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6"], - 6: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc"], - 7: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd"], - 8: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd", "#fddaec"], - 9: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd", "#fddaec", "#f2f2f2"] - }, - Pastel2: { - 3: ["#b3e2cd", "#fdcdac", "#cbd5e8"], - 4: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4"], - 5: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9"], - 6: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae"], - 7: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae", "#f1e2cc"], - 8: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae", "#f1e2cc", "#cccccc"] - }, - Set1: { - 3: ["#e41a1c", "#377eb8", "#4daf4a"], - 4: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3"], - 5: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"], - 6: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33"], - 7: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628"], - 8: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf"], - 9: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf", "#999999"] - }, - Set2: { - 3: ["#66c2a5", "#fc8d62", "#8da0cb"], - 4: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3"], - 5: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854"], - 6: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"], - 7: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494"], - 8: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494", "#b3b3b3"] - }, - Set3: { - 3: ["#8dd3c7", "#ffffb3", "#bebada"], - 4: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072"], - 5: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3"], - 6: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462"], - 7: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69"], - 8: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5"], - 9: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9"], - 10: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd"], - 11: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5"], - 12: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f"] - } -}; - -export class GeneralSettings extends FormattingSettingsSimpleCard { - public name: string = "general"; - public displayNameKey: string = "Visual_General"; - - public static DefaultColorbrewer: string = "Reds"; - public static BucketCountMaxLimit: number = 18; - public static BucketCountMinLimit: number = 1; - public static DefaultBucketCount: number = 5; - public static ColorbrewerMaxBucketCount: number = 14; - - public enableColorbrewer = new formattingSettings.ToggleSwitch({ - name: "enableColorbrewer", - displayNameKey: "Visual_EnableColorbrewer", - value: true, - }); - - public colorbrewer = new formattingSettings.AutoDropdown({ - name: "colorbrewer", - displayNameKey: "Visual_General_Colorbrewer", - value: "Reds", - }); - - public gradientStart = new formattingSettings.ColorPicker({ - name: "gradientStart", - displayNameKey: "Visual_GradientStart", - value: { value: "#FFFFFF" }, - }); - - public gradientEnd = new formattingSettings.ColorPicker({ - name: "gradientEnd", - displayNameKey: "Visual_GradientEnd", - value: { value: "#000000" }, - }); - - public invertColorScale = new formattingSettings.ToggleSwitch({ - name: "invertColorScale", - displayNameKey: "Visual_InvertColorScale", - descriptionKey: "Visual_Description_InvertColorScale", - value: false, - }); - - public fillNullValuesCells = new formattingSettings.ToggleSwitch({ - name: "fillNullValuesCells", - displayNameKey: "Visual_FillNullValCell", - value: true, - }); - - public buckets = new formattingSettings.NumUpDown({ - name: "buckets", - displayNameKey: "Visual_General_Granularity", - value: null, - options: { - minValue: { - type: powerbi.visuals.ValidatorType.Min, - value: GeneralSettings.BucketCountMinLimit - }, - maxValue: { - type: powerbi.visuals.ValidatorType.Max, - value: GeneralSettings.BucketCountMaxLimit - }, - } - }); - - public static stroke: string = "#E6E6E6"; - public textColor: string = "#AAAAAA"; - - public slices: FormattingSettingsSlice[] = [ - this.enableColorbrewer, - this.colorbrewer, - this.gradientStart, - this.gradientEnd, - this.invertColorScale, - this.fillNullValuesCells, - this.buckets - ]; -} - -export class BaseLabelCardSettings extends FormattingSettingsSimpleCard { - public static DefaultFontSize: number = 12; - private static MinFontSize: number = 8; - private static MaxFontSize: number = 60; - - public show = new formattingSettings.ToggleSwitch({ - name: "show", - displayNameKey: "Visual_Show", - value: true - }); - - public fill = new formattingSettings.ColorPicker({ - name: "fill", - displayNameKey: "Visual_LabelsFill", - value: { value: "#aaa" }, - }); - - public fontFamily: formattingSettings.FontPicker = new formattingSettings.FontPicker({ - name: `fontFamily`, - value: "Arial, sans-serif" - }); - - public fontSize: formattingSettings.NumUpDown = new formattingSettings.NumUpDown({ - name: `fontSize`, - displayName: "Text Size", - displayNameKey: "Visual_TextSize", - value: BaseLabelCardSettings.DefaultFontSize, - options: { - minValue: { - type: powerbi.visuals.ValidatorType.Min, - value: BaseLabelCardSettings.MinFontSize - }, - maxValue: { - type: powerbi.visuals.ValidatorType.Max, - value: BaseLabelCardSettings.MaxFontSize - } - } - }); - - public fontBold: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({ - name: `fontBold`, - value: false - }); - - public fontItalic: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({ - name: `fontItalic`, - value: false - }); - - public fontUnderline: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({ - name: `fontUnderline`, - value: false - }); - - protected font: formattingSettings.FontControl = new formattingSettings.FontControl({ - name: `font`, - displayName: "Font", - displayNameKey: "Visual_Font", - fontFamily: this.fontFamily, - fontSize: this.fontSize, - bold: this.fontBold, - italic: this.fontItalic, - underline: this.fontUnderline - }); - - constructor(name: string, displayNameKey: string, isShown: boolean = true) { - super(); - this.name = name; - this.displayNameKey = displayNameKey; - this.topLevelSlice = this.show; - this.slices = [this.font, this.fill]; - this.show.value = isShown; - } -} - -export class YAxisLabelsSettings extends BaseLabelCardSettings { - private static TextSymbolMinValue: number = 0; - private static TextSymbolMaxValue: number = 50; - - public maxTextSymbol = new formattingSettings.NumUpDown({ - name: "maxTextSymbol", - displayNameKey: "Visual_MaxTextSymbols", - value: 25, - options: { - minValue: { - type: powerbi.visuals.ValidatorType.Min, - value: YAxisLabelsSettings.TextSymbolMinValue - }, - maxValue: { - type: powerbi.visuals.ValidatorType.Max, - value: YAxisLabelsSettings.TextSymbolMaxValue - }, - } - }); - - public slices: FormattingSettingsSlice[] = [this.maxTextSymbol, this.font, this.fill]; - public topLevelSlice: formattingSettings.ToggleSwitch = this.show; -} - -export class SettingsModel extends FormattingSettingsModel { - public labels: BaseLabelCardSettings = new BaseLabelCardSettings("labels", "Visual_DataLabels", false); - public xAxisLabels: BaseLabelCardSettings = new BaseLabelCardSettings("xAxisLabels", "Visual_XAxis"); - public yAxisLabels: YAxisLabelsSettings = new YAxisLabelsSettings("yAxisLabels", "Visual_YAxis"); - public general: GeneralSettings = new GeneralSettings(); - - public cards: FormattingSettingsSimpleCard[] = [this.general, this.labels, this.xAxisLabels, this.yAxisLabels]; - - public CurrentBucketCount: number = GeneralSettings.BucketCountMinLimit; - - public initBuckets() { - if (this.general.enableColorbrewer.value) { - - if (this.general.colorbrewer.value === "") { - this.general.colorbrewer.value = GeneralSettings.DefaultColorbrewer; - } - - const colorbrewerArray: IColorArray = colorbrewer[this.general.colorbrewer.value]; - - let minBucketNum: number = 0; - let maxBucketNum: number = 0; - - for (let bucketIndex: number = GeneralSettings.BucketCountMinLimit; bucketIndex < GeneralSettings.ColorbrewerMaxBucketCount; bucketIndex++) { - const currentColorbrewerElement = colorbrewerArray[bucketIndex.toString()]; - - if (currentColorbrewerElement) { - if (minBucketNum === 0) { - minBucketNum = bucketIndex; - } - maxBucketNum = bucketIndex; - } - } - - const currentValue = this.general.buckets.value ?? GeneralSettings.DefaultBucketCount; - const clampedValue = Math.min(maxBucketNum, Math.max(currentValue, minBucketNum)); - - this.CurrentBucketCount = this.general.buckets.value = clampedValue; - this.general.buckets.options.minValue.value = minBucketNum; - this.general.buckets.options.maxValue.value = maxBucketNum; - } - else { - const currentValue = this.general.buckets.value ?? GeneralSettings.DefaultBucketCount; - const clampedValue = Math.min( - GeneralSettings.BucketCountMaxLimit, - Math.max(currentValue, GeneralSettings.BucketCountMinLimit) - ); - - this.CurrentBucketCount = this.general.buckets.value = clampedValue; - } - } +/* + * Power BI Visualizations + * + * 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. + */ +import { formattingSettings } from "powerbi-visuals-utils-formattingmodel"; + +import FormattingSettingsSimpleCard = formattingSettings.SimpleCard; +import FormattingSettingsCompositeCard = formattingSettings.CompositeCard; +import FormattingSettingsCard = formattingSettings.Cards; +import FormattingSettingsGroup = formattingSettings.Group; +import FormattingSettingsSlice = formattingSettings.Slice; +import FormattingSettingsModel = formattingSettings.Model; + +import { IColorArray, IColorBrewer } from "./dataInterfaces"; +import powerbi from "powerbi-visuals-api"; + +export const colorbrewer: IColorBrewer = { + YlGn: { + 3: ["#f7fcb9", "#addd8e", "#31a354"], + 4: ["#ffffcc", "#c2e699", "#78c679", "#238443"], + 5: ["#ffffcc", "#c2e699", "#78c679", "#31a354", "#006837"], + 6: ["#ffffcc", "#d9f0a3", "#addd8e", "#78c679", "#31a354", "#006837"], + 7: ["#ffffcc", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#005a32"], + 8: ["#ffffe5", "#f7fcb9", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#005a32"], + 9: ["#ffffe5", "#f7fcb9", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#006837", "#004529"] + }, + YlGnBu: { + 3: ["#edf8b1", "#7fcdbb", "#2c7fb8"], + 4: ["#ffffcc", "#a1dab4", "#41b6c4", "#225ea8"], + 5: ["#ffffcc", "#a1dab4", "#41b6c4", "#2c7fb8", "#253494"], + 6: ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#2c7fb8", "#253494"], + 7: ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"], + 8: ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"], + 9: ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#253494", "#081d58"] + }, + GnBu: { + 3: ["#e0f3db", "#a8ddb5", "#43a2ca"], + 4: ["#f0f9e8", "#bae4bc", "#7bccc4", "#2b8cbe"], + 5: ["#f0f9e8", "#bae4bc", "#7bccc4", "#43a2ca", "#0868ac"], + 6: ["#f0f9e8", "#ccebc5", "#a8ddb5", "#7bccc4", "#43a2ca", "#0868ac"], + 7: ["#f0f9e8", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#08589e"], + 8: ["#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#08589e"], + 9: ["#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081"] + }, + BuGn: { + 3: ["#e5f5f9", "#99d8c9", "#2ca25f"], + 4: ["#edf8fb", "#b2e2e2", "#66c2a4", "#238b45"], + 5: ["#edf8fb", "#b2e2e2", "#66c2a4", "#2ca25f", "#006d2c"], + 6: ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#2ca25f", "#006d2c"], + 7: ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"], + 8: ["#f7fcfd", "#e5f5f9", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"], + 9: ["#f7fcfd", "#e5f5f9", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#006d2c", "#00441b"] + }, + PuBuGn: { + 3: ["#ece2f0", "#a6bddb", "#1c9099"], + 4: ["#f6eff7", "#bdc9e1", "#67a9cf", "#02818a"], + 5: ["#f6eff7", "#bdc9e1", "#67a9cf", "#1c9099", "#016c59"], + 6: ["#f6eff7", "#d0d1e6", "#a6bddb", "#67a9cf", "#1c9099", "#016c59"], + 7: ["#f6eff7", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"], + 8: ["#fff7fb", "#ece2f0", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"], + 9: ["#fff7fb", "#ece2f0", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016c59", "#014636"] + }, + PuBu: { + 3: ["#ece7f2", "#a6bddb", "#2b8cbe"], + 4: ["#f1eef6", "#bdc9e1", "#74a9cf", "#0570b0"], + 5: ["#f1eef6", "#bdc9e1", "#74a9cf", "#2b8cbe", "#045a8d"], + 6: ["#f1eef6", "#d0d1e6", "#a6bddb", "#74a9cf", "#2b8cbe", "#045a8d"], + 7: ["#f1eef6", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#034e7b"], + 8: ["#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#034e7b"], + 9: ["#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#045a8d", "#023858"] + }, + BuPu: { + 3: ["#e0ecf4", "#9ebcda", "#8856a7"], + 4: ["#edf8fb", "#b3cde3", "#8c96c6", "#88419d"], + 5: ["#edf8fb", "#b3cde3", "#8c96c6", "#8856a7", "#810f7c"], + 6: ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8856a7", "#810f7c"], + 7: ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"], + 8: ["#f7fcfd", "#e0ecf4", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"], + 9: ["#f7fcfd", "#e0ecf4", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#810f7c", "#4d004b"] + }, + RdPu: { + 3: ["#fde0dd", "#fa9fb5", "#c51b8a"], + 4: ["#feebe2", "#fbb4b9", "#f768a1", "#ae017e"], + 5: ["#feebe2", "#fbb4b9", "#f768a1", "#c51b8a", "#7a0177"], + 6: ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#c51b8a", "#7a0177"], + 7: ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177"], + 8: ["#fff7f3", "#fde0dd", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177"], + 9: ["#fff7f3", "#fde0dd", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177", "#49006a"] + }, + PuRd: { + 3: ["#e7e1ef", "#c994c7", "#dd1c77"], + 4: ["#f1eef6", "#d7b5d8", "#df65b0", "#ce1256"], + 5: ["#f1eef6", "#d7b5d8", "#df65b0", "#dd1c77", "#980043"], + 6: ["#f1eef6", "#d4b9da", "#c994c7", "#df65b0", "#dd1c77", "#980043"], + 7: ["#f1eef6", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"], + 8: ["#f7f4f9", "#e7e1ef", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"], + 9: ["#f7f4f9", "#e7e1ef", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#980043", "#67001f"] + }, + OrRd: { + 3: ["#fee8c8", "#fdbb84", "#e34a33"], + 4: ["#fef0d9", "#fdcc8a", "#fc8d59", "#d7301f"], + 5: ["#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33", "#b30000"], + 6: ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#e34a33", "#b30000"], + 7: ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"], + 8: ["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"], + 9: ["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"] + }, + YlOrRd: { + 3: ["#ffeda0", "#feb24c", "#f03b20"], + 4: ["#ffffb2", "#fecc5c", "#fd8d3c", "#e31a1c"], + 5: ["#ffffb2", "#fecc5c", "#fd8d3c", "#f03b20", "#bd0026"], + 6: ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#f03b20", "#bd0026"], + 7: ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"], + 8: ["#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"], + 9: ["#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#bd0026", "#800026"] + }, + YlOrBr: { + 3: ["#fff7bc", "#fec44f", "#d95f0e"], + 4: ["#ffffd4", "#fed98e", "#fe9929", "#cc4c02"], + 5: ["#ffffd4", "#fed98e", "#fe9929", "#d95f0e", "#993404"], + 6: ["#ffffd4", "#fee391", "#fec44f", "#fe9929", "#d95f0e", "#993404"], + 7: ["#ffffd4", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#8c2d04"], + 8: ["#ffffe5", "#fff7bc", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#8c2d04"], + 9: ["#ffffe5", "#fff7bc", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#993404", "#662506"] + }, + Purples: { + 3: ["#efedf5", "#bcbddc", "#756bb1"], + 4: ["#f2f0f7", "#cbc9e2", "#9e9ac8", "#6a51a3"], + 5: ["#f2f0f7", "#cbc9e2", "#9e9ac8", "#756bb1", "#54278f"], + 6: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"], + 7: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#4a1486"], + 8: ["#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#4a1486"], + 9: ["#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#54278f", "#3f007d"] + }, + Blues: { + 3: ["#deebf7", "#9ecae1", "#3182bd"], + 4: ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"], + 5: ["#eff3ff", "#bdd7e7", "#6baed6", "#3182bd", "#08519c"], + 6: ["#eff3ff", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"], + 7: ["#eff3ff", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"], + 8: ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"], + 9: ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#08519c", "#08306b"] + }, + Greens: { + 3: ["#e5f5e0", "#a1d99b", "#31a354"], + 4: ["#edf8e9", "#bae4b3", "#74c476", "#238b45"], + 5: ["#edf8e9", "#bae4b3", "#74c476", "#31a354", "#006d2c"], + 6: ["#edf8e9", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"], + 7: ["#edf8e9", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#005a32"], + 8: ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#005a32"], + 9: ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#006d2c", "#00441b"] + }, + Oranges: { + 3: ["#fee6ce", "#fdae6b", "#e6550d"], + 4: ["#feedde", "#fdbe85", "#fd8d3c", "#d94701"], + 5: ["#feedde", "#fdbe85", "#fd8d3c", "#e6550d", "#a63603"], + 6: ["#feedde", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"], + 7: ["#feedde", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#8c2d04"], + 8: ["#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#8c2d04"], + 9: ["#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#a63603", "#7f2704"] + }, + Reds: { + 3: ["#fee0d2", "#fc9272", "#de2d26"], + 4: ["#fee5d9", "#fcae91", "#fb6a4a", "#cb181d"], + 5: ["#fee5d9", "#fcae91", "#fb6a4a", "#de2d26", "#a50f15"], + 6: ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#de2d26", "#a50f15"], + 7: ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"], + 8: ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"], + 9: ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#a50f15", "#67000d"] + }, + Greys: { + 3: ["#f0f0f0", "#bdbdbd", "#636363"], + 4: ["#f7f7f7", "#cccccc", "#969696", "#525252"], + 5: ["#f7f7f7", "#cccccc", "#969696", "#636363", "#252525"], + 6: ["#f7f7f7", "#d9d9d9", "#bdbdbd", "#969696", "#636363", "#252525"], + 7: ["#f7f7f7", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525"], + 8: ["#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525"], + 9: ["#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525", "#000000"] + }, + PuOr: { + 3: ["#f1a340", "#f7f7f7", "#998ec3"], + 4: ["#e66101", "#fdb863", "#b2abd2", "#5e3c99"], + 5: ["#e66101", "#fdb863", "#f7f7f7", "#b2abd2", "#5e3c99"], + 6: ["#b35806", "#f1a340", "#fee0b6", "#d8daeb", "#998ec3", "#542788"], + 7: ["#b35806", "#f1a340", "#fee0b6", "#f7f7f7", "#d8daeb", "#998ec3", "#542788"], + 8: ["#b35806", "#e08214", "#fdb863", "#fee0b6", "#d8daeb", "#b2abd2", "#8073ac", "#542788"], + 9: ["#b35806", "#e08214", "#fdb863", "#fee0b6", "#f7f7f7", "#d8daeb", "#b2abd2", "#8073ac", "#542788"], + 10: ["#7f3b08", "#b35806", "#e08214", "#fdb863", "#fee0b6", "#d8daeb", "#b2abd2", "#8073ac", "#542788", "#2d004b"], + 11: ["#7f3b08", "#b35806", "#e08214", "#fdb863", "#fee0b6", "#f7f7f7", "#d8daeb", "#b2abd2", "#8073ac", "#542788", "#2d004b"] + }, + BrBG: { + 3: ["#d8b365", "#f5f5f5", "#5ab4ac"], + 4: ["#a6611a", "#dfc27d", "#80cdc1", "#018571"], + 5: ["#a6611a", "#dfc27d", "#f5f5f5", "#80cdc1", "#018571"], + 6: ["#8c510a", "#d8b365", "#f6e8c3", "#c7eae5", "#5ab4ac", "#01665e"], + 7: ["#8c510a", "#d8b365", "#f6e8c3", "#f5f5f5", "#c7eae5", "#5ab4ac", "#01665e"], + 8: ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#c7eae5", "#80cdc1", "#35978f", "#01665e"], + 9: ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e"], + 10: ["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"], + 11: ["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"] + }, + PRGn: { + 3: ["#af8dc3", "#f7f7f7", "#7fbf7b"], + 4: ["#7b3294", "#c2a5cf", "#a6dba0", "#008837"], + 5: ["#7b3294", "#c2a5cf", "#f7f7f7", "#a6dba0", "#008837"], + 6: ["#762a83", "#af8dc3", "#e7d4e8", "#d9f0d3", "#7fbf7b", "#1b7837"], + 7: ["#762a83", "#af8dc3", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#7fbf7b", "#1b7837"], + 8: ["#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837"], + 9: ["#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837"], + 10: ["#40004b", "#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837", "#00441b"], + 11: ["#40004b", "#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837", "#00441b"] + }, + PiYG: { + 3: ["#e9a3c9", "#f7f7f7", "#a1d76a"], + 4: ["#d01c8b", "#f1b6da", "#b8e186", "#4dac26"], + 5: ["#d01c8b", "#f1b6da", "#f7f7f7", "#b8e186", "#4dac26"], + 6: ["#c51b7d", "#e9a3c9", "#fde0ef", "#e6f5d0", "#a1d76a", "#4d9221"], + 7: ["#c51b7d", "#e9a3c9", "#fde0ef", "#f7f7f7", "#e6f5d0", "#a1d76a", "#4d9221"], + 8: ["#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221"], + 9: ["#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221"], + 10: ["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"], + 11: ["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"] + }, + RdBu: { + 3: ["#ef8a62", "#f7f7f7", "#67a9cf"], + 4: ["#ca0020", "#f4a582", "#92c5de", "#0571b0"], + 5: ["#ca0020", "#f4a582", "#f7f7f7", "#92c5de", "#0571b0"], + 6: ["#b2182b", "#ef8a62", "#fddbc7", "#d1e5f0", "#67a9cf", "#2166ac"], + 7: ["#b2182b", "#ef8a62", "#fddbc7", "#f7f7f7", "#d1e5f0", "#67a9cf", "#2166ac"], + 8: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac"], + 9: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac"], + 10: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"], + 11: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"] + }, + RdGy: { + 3: ["#ef8a62", "#ffffff", "#999999"], + 4: ["#ca0020", "#f4a582", "#bababa", "#404040"], + 5: ["#ca0020", "#f4a582", "#ffffff", "#bababa", "#404040"], + 6: ["#b2182b", "#ef8a62", "#fddbc7", "#e0e0e0", "#999999", "#4d4d4d"], + 7: ["#b2182b", "#ef8a62", "#fddbc7", "#ffffff", "#e0e0e0", "#999999", "#4d4d4d"], + 8: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#e0e0e0", "#bababa", "#878787", "#4d4d4d"], + 9: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#ffffff", "#e0e0e0", "#bababa", "#878787", "#4d4d4d"], + 10: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#e0e0e0", "#bababa", "#878787", "#4d4d4d", "#1a1a1a"], + 11: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#ffffff", "#e0e0e0", "#bababa", "#878787", "#4d4d4d", "#1a1a1a"] + }, + RdYlBu: { + 3: ["#fc8d59", "#ffffbf", "#91bfdb"], + 4: ["#d7191c", "#fdae61", "#abd9e9", "#2c7bb6"], + 5: ["#d7191c", "#fdae61", "#ffffbf", "#abd9e9", "#2c7bb6"], + 6: ["#d73027", "#fc8d59", "#fee090", "#e0f3f8", "#91bfdb", "#4575b4"], + 7: ["#d73027", "#fc8d59", "#fee090", "#ffffbf", "#e0f3f8", "#91bfdb", "#4575b4"], + 8: ["#d73027", "#f46d43", "#fdae61", "#fee090", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"], + 9: ["#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"], + 10: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee090", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"], + 11: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"] + }, + Spectral: { + 3: ["#fc8d59", "#ffffbf", "#99d594"], + 4: ["#d7191c", "#fdae61", "#abdda4", "#2b83ba"], + 5: ["#d7191c", "#fdae61", "#ffffbf", "#abdda4", "#2b83ba"], + 6: ["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#99d594", "#3288bd"], + 7: ["#d53e4f", "#fc8d59", "#fee08b", "#ffffbf", "#e6f598", "#99d594", "#3288bd"], + 8: ["#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#e6f598", "#abdda4", "#66c2a5", "#3288bd"], + 9: ["#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd"], + 10: ["#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"], + 11: ["#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"] + }, + RdYlGn: { + 3: ["#fc8d59", "#ffffbf", "#91cf60"], + 4: ["#d7191c", "#fdae61", "#a6d96a", "#1a9641"], + 5: ["#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641"], + 6: ["#d73027", "#fc8d59", "#fee08b", "#d9ef8b", "#91cf60", "#1a9850"], + 7: ["#d73027", "#fc8d59", "#fee08b", "#ffffbf", "#d9ef8b", "#91cf60", "#1a9850"], + 8: ["#d73027", "#f46d43", "#fdae61", "#fee08b", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850"], + 9: ["#d73027", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850"], + 10: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"], + 11: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"] + }, + Accent: { + 3: ["#7fc97f", "#beaed4", "#fdc086"], + 4: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99"], + 5: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0"], + 6: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f"], + 7: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17"], + 8: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17", "#666666"] + }, + Dark2: { + 3: ["#1b9e77", "#d95f02", "#7570b3"], + 4: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a"], + 5: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e"], + 6: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02"], + 7: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d"], + 8: ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d", "#666666"] + }, + Paired: { + 3: ["#a6cee3", "#1f78b4", "#b2df8a"], + 4: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"], + 5: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99"], + 6: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c"], + 7: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f"], + 8: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00"], + 9: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6"], + 10: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a"], + 11: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99"], + 12: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99", "#b15928"] + }, + Pastel1: { + 3: ["#fbb4ae", "#b3cde3", "#ccebc5"], + 4: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4"], + 5: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6"], + 6: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc"], + 7: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd"], + 8: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd", "#fddaec"], + 9: ["#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd", "#fddaec", "#f2f2f2"] + }, + Pastel2: { + 3: ["#b3e2cd", "#fdcdac", "#cbd5e8"], + 4: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4"], + 5: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9"], + 6: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae"], + 7: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae", "#f1e2cc"], + 8: ["#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae", "#f1e2cc", "#cccccc"] + }, + Set1: { + 3: ["#e41a1c", "#377eb8", "#4daf4a"], + 4: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3"], + 5: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"], + 6: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33"], + 7: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628"], + 8: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf"], + 9: ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf", "#999999"] + }, + Set2: { + 3: ["#66c2a5", "#fc8d62", "#8da0cb"], + 4: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3"], + 5: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854"], + 6: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"], + 7: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494"], + 8: ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494", "#b3b3b3"] + }, + Set3: { + 3: ["#8dd3c7", "#ffffb3", "#bebada"], + 4: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072"], + 5: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3"], + 6: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462"], + 7: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69"], + 8: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5"], + 9: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9"], + 10: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd"], + 11: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5"], + 12: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f"] + } +}; + +export class GeneralSettings extends FormattingSettingsCompositeCard { + public name: string = "general"; + public displayNameKey: string = "Visual_General"; + + public static DefaultColorbrewer: string = "Reds"; + public static BucketCountMaxLimit: number = 18; + public static BucketCountMinLimit: number = 1; + public static BucketCountMinLimitWithGradientMiddle: number = 3; + public static DefaultBucketCount: number = 5; + public static ColorbrewerMaxBucketCount: number = 14; + + public enableColorbrewer = new formattingSettings.ToggleSwitch({ + name: "enableColorbrewer", + displayNameKey: "Visual_EnableColorbrewer", + value: true, + }); + + public colorbrewer = new formattingSettings.AutoDropdown({ + name: "colorbrewer", + displayNameKey: "Visual_General_Colorbrewer", + value: "Reds", + }); + + public gradientStart = new formattingSettings.ColorPicker({ + name: "gradientStart", + displayNameKey: "Visual_GradientStart", + value: { value: "#FFFFFF" }, + }); + + public gradientEnd = new formattingSettings.ColorPicker({ + name: "gradientEnd", + displayNameKey: "Visual_GradientEnd", + value: { value: "#000000" }, + }); + + public activateGradientMiddle = new formattingSettings.ToggleSwitch({ + name: "activateGradientMiddle", + displayNameKey: "Visual_ActivateGradientMiddle", + value: false, + }); + + public gradientMiddle = new formattingSettings.ColorPicker({ + name: "gradientMiddle", + displayNameKey: "Visual_GradientMiddle", + value: { value: "#767676" }, + }); + + public invertColorScale = new formattingSettings.ToggleSwitch({ + name: "invertColorScale", + displayNameKey: "Visual_InvertColorScale", + descriptionKey: "Visual_Description_InvertColorScale", + value: false, + }); + + public fillNullValuesCells = new formattingSettings.ToggleSwitch({ + name: "fillNullValuesCells", + displayNameKey: "Visual_FillNullValCell", + value: true, + }); + + public buckets = new formattingSettings.NumUpDown({ + name: "buckets", + displayNameKey: "Visual_General_Granularity", + value: null, + options: { + minValue: { + type: powerbi.visuals.ValidatorType.Min, + value: GeneralSettings.BucketCountMinLimit + }, + maxValue: { + type: powerbi.visuals.ValidatorType.Max, + value: GeneralSettings.BucketCountMaxLimit + }, + } + }); + + public stroke: string = "#E6E6E6"; + public textColor: string = "#AAAAAA"; + + private paletteGroup: FormattingSettingsGroup = new formattingSettings.Group({ + name: "paletteGroup", + displayNameKey: "Visual_General_Colorbrewer", + collapsible: false, + topLevelSlice: this.enableColorbrewer, + slices: [this.colorbrewer], + }); + + private gradientGroup: FormattingSettingsGroup = new formattingSettings.Group({ + name: "gradientGroup", + displayNameKey: "Visual_General_Gradient", + collapsible: false, + slices: [this.activateGradientMiddle, this.gradientStart, this.gradientMiddle, this.gradientEnd], + }); + + private gradientScaleGroup: FormattingSettingsGroup = new formattingSettings.Group({ + name: "gradientScaleGroup", + displayNameKey: "Visual_General_Additional", + collapsible: false, + slices: [this.buckets, this.fillNullValuesCells, this.invertColorScale], + }); + + public groups: FormattingSettingsGroup[] = [this.paletteGroup, this.gradientGroup, this.gradientScaleGroup]; + + public onPreProcess(): void { + this.gradientMiddle.visible = this.activateGradientMiddle.value; + } +} + +export class BaseLabelCardSettings extends FormattingSettingsSimpleCard { + public static DefaultFontSize: number = 12; + private static MinFontSize: number = 8; + private static MaxFontSize: number = 60; + + public show = new formattingSettings.ToggleSwitch({ + name: "show", + displayNameKey: "Visual_Show", + value: true + }); + + public fill = new formattingSettings.ColorPicker({ + name: "fill", + displayNameKey: "Visual_LabelsFill", + value: { value: "#aaa" }, + }); + + public fontFamily: formattingSettings.FontPicker = new formattingSettings.FontPicker({ + name: `fontFamily`, + value: "Arial, sans-serif" + }); + + public fontSize: formattingSettings.NumUpDown = new formattingSettings.NumUpDown({ + name: `fontSize`, + displayName: "Text Size", + displayNameKey: "Visual_TextSize", + value: BaseLabelCardSettings.DefaultFontSize, + options: { + minValue: { + type: powerbi.visuals.ValidatorType.Min, + value: BaseLabelCardSettings.MinFontSize + }, + maxValue: { + type: powerbi.visuals.ValidatorType.Max, + value: BaseLabelCardSettings.MaxFontSize + } + } + }); + + public fontBold: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({ + name: `fontBold`, + value: false + }); + + public fontItalic: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({ + name: `fontItalic`, + value: false + }); + + public fontUnderline: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({ + name: `fontUnderline`, + value: false + }); + + protected font: formattingSettings.FontControl = new formattingSettings.FontControl({ + name: `font`, + displayName: "Font", + displayNameKey: "Visual_Font", + fontFamily: this.fontFamily, + fontSize: this.fontSize, + bold: this.fontBold, + italic: this.fontItalic, + underline: this.fontUnderline + }); + + constructor(name: string, displayNameKey: string, isShown: boolean = true) { + super(); + this.name = name; + this.displayNameKey = displayNameKey; + this.topLevelSlice = this.show; + this.slices = [this.font, this.fill]; + this.show.value = isShown; + } +} + +export class YAxisLabelsSettings extends BaseLabelCardSettings { + private static TextSymbolMinValue: number = 0; + private static TextSymbolMaxValue: number = 50; + + public maxTextSymbol = new formattingSettings.NumUpDown({ + name: "maxTextSymbol", + displayNameKey: "Visual_MaxTextSymbols", + value: 25, + options: { + minValue: { + type: powerbi.visuals.ValidatorType.Min, + value: YAxisLabelsSettings.TextSymbolMinValue + }, + maxValue: { + type: powerbi.visuals.ValidatorType.Max, + value: YAxisLabelsSettings.TextSymbolMaxValue + }, + } + }); + + public slices: FormattingSettingsSlice[] = [this.maxTextSymbol, this.font, this.fill]; + public topLevelSlice: formattingSettings.ToggleSwitch = this.show; +} + +export class SettingsModel extends FormattingSettingsModel { + public labels: BaseLabelCardSettings = new BaseLabelCardSettings("labels", "Visual_DataLabels", false); + public xAxisLabels: BaseLabelCardSettings = new BaseLabelCardSettings("xAxisLabels", "Visual_XAxis"); + public yAxisLabels: YAxisLabelsSettings = new YAxisLabelsSettings("yAxisLabels", "Visual_YAxis"); + public general: GeneralSettings = new GeneralSettings(); + + public cards: FormattingSettingsCard[] = [this.general, this.labels, this.xAxisLabels, this.yAxisLabels]; + + public CurrentBucketCount: number = GeneralSettings.BucketCountMinLimit; + + public initBuckets() { + if (this.general.enableColorbrewer.value) { + + if (this.general.colorbrewer.value === "") { + this.general.colorbrewer.value = GeneralSettings.DefaultColorbrewer; + } + + const colorbrewerArray: IColorArray = colorbrewer[this.general.colorbrewer.value]; + + let minBucketNum: number = 0; + let maxBucketNum: number = 0; + + for (let bucketIndex: number = GeneralSettings.BucketCountMinLimit; bucketIndex < GeneralSettings.ColorbrewerMaxBucketCount; bucketIndex++) { + const currentColorbrewerElement = colorbrewerArray[bucketIndex.toString()]; + + if (currentColorbrewerElement) { + if (minBucketNum === 0) { + minBucketNum = bucketIndex; + } + maxBucketNum = bucketIndex; + } + } + + const currentValue = this.general.buckets.value ?? GeneralSettings.DefaultBucketCount; + const clampedValue = Math.min(maxBucketNum, Math.max(currentValue, minBucketNum)); + + this.CurrentBucketCount = this.general.buckets.value = clampedValue; + this.general.buckets.options.minValue.value = minBucketNum; + this.general.buckets.options.maxValue.value = maxBucketNum; + } + else { + const minLimit = this.general.activateGradientMiddle.value + ? GeneralSettings.BucketCountMinLimitWithGradientMiddle + : GeneralSettings.BucketCountMinLimit; + const currentValue = this.general.buckets.value ?? GeneralSettings.DefaultBucketCount; + const clampedValue = Math.min( + GeneralSettings.BucketCountMaxLimit, + Math.max(currentValue, minLimit) + ); + + this.CurrentBucketCount = this.general.buckets.value = clampedValue; + this.general.buckets.options.minValue.value = minLimit; + this.general.buckets.options.maxValue.value = GeneralSettings.BucketCountMaxLimit; + } + } } \ No newline at end of file diff --git a/src/visual.ts b/src/visual.ts index 4fd99ac..dc9ba70 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -46,8 +46,6 @@ import { min as d3Min, max as d3Max } from "d3-array"; import "d3-transition"; -import maxBy from "lodash.maxby"; - import { pixelConverter as PixelConverter } from "powerbi-visuals-utils-typeutils"; import IVisualHost = powerbi.extensibility.visual.IVisualHost; @@ -77,12 +75,23 @@ import { import { BaseLabelCardSettings, - GeneralSettings, SettingsModel, - YAxisLabelsSettings, colorbrewer } from "./settings"; +import { + calculateGridSizeHeight, + calculateGridSizeWidth, + CellMaxHeightLimit, + getXAxisHeight, + getYAxisHeight, + getYAxisWidth, + isDataViewValid, + parseSettings, + resolveStartEndColors, + textLimit +} from "./heatmapUtils"; + import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel"; import { @@ -109,9 +118,7 @@ export class TableHeatMap implements IVisual { private viewport: IViewport; private behavior: VisualWebBehavior; private static Margin: IMargin = { left: 5, right: 10, bottom: 15, top: 10 }; - private static AdditionalSpaceForColorbrewerCells: number = 2; - private static YAxisAdditinalMargin: number = 5; private animationDuration: number = 1000; private static ClsAll: string = "*"; @@ -150,14 +157,11 @@ export class TableHeatMap implements IVisual { private static ConstEnd: string = "end"; private static ConstBegin: string = "begin"; private static ConstMiddle: string = "middle"; - private static Const0em: string = "0em"; private static Const071em: string = ".71em"; private static ConstGridSizeWidthLimit: number = 80; private static ConstShiftLabelFromGrid: number = -6; private static ConstGridHeightWidthRatio: number = 0.5; - private static ConstGridMinHeight: number = 5; - private static ConstGridMinWidth: number = 1; private static ConstGridLegendWidthRatio: number = 0.925; private static ConstLegendOffsetFromChartByY: number = 0.5; private static ConstRectWidthAdjustment: number = 1; @@ -166,8 +170,7 @@ export class TableHeatMap implements IVisual { private static LegendTextFontSize = 12; private static LegendTextFontFamily = "'Segoe UI', wf_segoe-ui_normal, helvetica, arial, sans-serif"; - public static CellMaxHeightLimit: number = 300; - private static CellMaxWidthFactorLimit: number = 15; + public static CellMaxHeightLimit: number = CellMaxHeightLimit; private selectionManager: ISelectionManager; @@ -312,7 +315,7 @@ export class TableHeatMap implements IVisual { this.mainGraphics = this.svg.append(TableHeatMap.HtmlObjG); this.setSize(options.viewport); - const isValid = this.isDataViewValid(options.dataViews[0]); + const isValid = isDataViewValid(options.dataViews[0]); if (!isValid) { this.renderNotEnoughDataMessage(options); this.host.eventService.renderingFinished(options); @@ -323,7 +326,7 @@ export class TableHeatMap implements IVisual { this.settingsModel = this.formattingSettingsService.populateFormattingSettingsModel(SettingsModel, options.dataViews[0]); this.settingsModel.initBuckets(); - this.settingsModel = TableHeatMap.parseSettings(this.colorHelper, this.settingsModel); + this.settingsModel = parseSettings(this.colorHelper, this.settingsModel); this.render(this.converter(options.dataViews[0]), this.settingsModel, options.viewport); this.host.eventService.renderingFinished(options); @@ -333,10 +336,6 @@ export class TableHeatMap implements IVisual { } } - private isDataViewValid(dataView: powerbi.DataView): boolean { - return !!(dataView.categorical?.categories && dataView.categorical?.values); - } - private renderNotEnoughDataMessage(options: VisualUpdateOptions): void { const createMessageGroup = (title: string, message: string, currentOffsetY: number) => { const group = this.mainGraphics.append("g") @@ -377,29 +376,6 @@ export class TableHeatMap implements IVisual { } } - private static getYAxisWidth(chartData: TableHeatMapChartData, settings: YAxisLabelsSettings): number { - let maxLengthText: powerbi.PrimitiveValue = maxBy(chartData.categoryY, (d) => String(d).length) || ""; - - maxLengthText = TableHeatMap.textLimit(maxLengthText.toString(), settings.maxTextSymbol.value); - - return settings.show.value ? textMeasurementService.measureSvgTextWidth({ - fontSize: PixelConverter.toString(settings.fontSize.value), - text: maxLengthText.trim(), - fontFamily: settings.fontFamily.value.toString() - }) + TableHeatMap.YAxisAdditinalMargin : 0; - } - - private static getXAxisHeight(chartData: TableHeatMapChartData, settings: BaseLabelCardSettings): number { - const categoryX: string[] = chartData.categoryX.map(x => x?.toString() ?? ""); - const maxLengthText: powerbi.PrimitiveValue = maxBy(categoryX, "length") || ""; - - return settings.show.value ? textMeasurementService.measureSvgTextHeight({ - fontSize: PixelConverter.toString(settings.fontSize.value), - text: maxLengthText.toString().trim(), - fontFamily: settings.fontFamily.value.toString() - }) : 0; - } - private processViewMode(options: VisualUpdateOptions): void { const { viewMode, dataViews } = options; const hasSeries = dataViews[0].metadata.columns.some(col => col.roles["Series"]); @@ -431,54 +407,6 @@ export class TableHeatMap implements IVisual { } } - private getYAxisHeight(chartData: TableHeatMapChartData): number { - const maxLengthText: powerbi.PrimitiveValue = maxBy(chartData.categoryY, (d) => String(d).length) || ""; - - return textMeasurementService.measureSvgTextHeight({ - fontSize: PixelConverter.toString(this.settingsModel.yAxisLabels.fontSize.value), - text: maxLengthText.toString().trim(), - fontFamily: this.settingsModel.yAxisLabels.fontFamily.value.toString() - }); - } - - private static parseSettings(colorHelper: ColorHelper, settingsModel: SettingsModel): SettingsModel { - if (colorHelper.isHighContrast) { - const foregroundColor: string = colorHelper.getThemeColor("foreground"); - const backgroundColor: string = colorHelper.getThemeColor("background"); - - settingsModel.labels.show.value = true; - settingsModel.labels.fill.value.value = foregroundColor; - - settingsModel.xAxisLabels.fill.value.value = foregroundColor; - settingsModel.yAxisLabels.fill.value.value = foregroundColor; - - settingsModel.general.enableColorbrewer.value = false; - settingsModel.general.gradientStart.value.value = backgroundColor; - settingsModel.general.gradientEnd.value.value = backgroundColor; - GeneralSettings.stroke = foregroundColor; - settingsModel.general.textColor = foregroundColor; - } - - return settingsModel; - } - - private getGridSizeHeight(xAxisHeight: number, length: number): number { - const gridSizeHeight: number = Math.floor((this.viewport.height - TableHeatMap.Margin.top - xAxisHeight - TableHeatMap.Margin.bottom - TableHeatMap.YAxisAdditinalMargin) / (length + TableHeatMap.AdditionalSpaceForColorbrewerCells)); - - return Math.max( - TableHeatMap.ConstGridMinHeight, - Math.min(gridSizeHeight, TableHeatMap.CellMaxHeightLimit)); - } - - private getGridSizeWidth(yAxisWidth: number, length: number, gridSizeHeight: number): number { - const gridSizeWidth: number = Math.floor((this.viewport.width - yAxisWidth) / (length)); - - return Math.max( - TableHeatMap.ConstGridMinWidth, - Math.min(gridSizeWidth, gridSizeHeight * TableHeatMap.CellMaxWidthFactorLimit) - ); - } - private render(chartData: TableHeatMapChartData, settingsModel: SettingsModel, viewport: IViewport): void { if (chartData.dataPoints) { const renderOptions: IRenderOptions = this.createRenderOptions(chartData, settingsModel); @@ -505,15 +433,15 @@ export class TableHeatMap implements IVisual { } private createRenderOptions(chartData: TableHeatMapChartData, settingsModel: SettingsModel): IRenderOptions { - const xAxisHeight: number = TableHeatMap.getXAxisHeight(chartData, settingsModel.xAxisLabels); - const yAxisWidth: number = TableHeatMap.getYAxisWidth(chartData, settingsModel.yAxisLabels); - const yAxisHeight: number = this.getYAxisHeight(chartData); + const xAxisHeight: number = getXAxisHeight(chartData, settingsModel.xAxisLabels); + const yAxisWidth: number = getYAxisWidth(chartData, settingsModel.yAxisLabels); + const yAxisHeight: number = getYAxisHeight(chartData, settingsModel.yAxisLabels); const xOffset: number = TableHeatMap.Margin.left + yAxisWidth; const yOffset: number = TableHeatMap.Margin.top + xAxisHeight; - const gridSizeHeight: number = this.getGridSizeHeight(xAxisHeight, chartData.categoryY.length); - const gridSizeWidth: number = this.getGridSizeWidth(yAxisWidth, chartData.categoryX.length, gridSizeHeight); + const gridSizeHeight: number = calculateGridSizeHeight(this.viewport.height, xAxisHeight, chartData.categoryY.length, TableHeatMap.Margin.top, TableHeatMap.Margin.bottom); + const gridSizeWidth: number = calculateGridSizeWidth(this.viewport.width, yAxisWidth, chartData.categoryX.length, gridSizeHeight); const minDataValue: number = d3Min(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); const maxDataValue: number = d3Max(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); @@ -562,13 +490,40 @@ export class TableHeatMap implements IVisual { private initColors(settingsModel: SettingsModel): string[] { const colorbrewerScale: string = settingsModel.general.colorbrewer.value.toString(); const colorbrewerEnable: boolean = settingsModel.general.enableColorbrewer.value; + const activateGradientMiddle: boolean = settingsModel.general.activateGradientMiddle.value; const numBuckets: number = settingsModel.CurrentBucketCount; + if (activateGradientMiddle) { + const { startColor, endColor } = resolveStartEndColors( + colorbrewerEnable, + colorbrewerScale, + numBuckets, + settingsModel.general.gradientStart.value.value, + settingsModel.general.gradientEnd.value.value + ); + + const middleColor: string = settingsModel.general.gradientMiddle.value.value; + const mid: number = (numBuckets - 1) / 2; + const domain: number[] = [0, mid, numBuckets - 1]; + const range: string[] = [startColor, middleColor, endColor]; + const colorScale: LinearColorScale = createLinearColorScale(domain, range, true); + const colors: string[] = []; + + for (let i: number = 0; i < numBuckets; i++) { + colors.push(colorScale(i)); + } + + return colors; + } + if (colorbrewerEnable) { - const currentColorbrewer: IColorArray = colorbrewerScale ? colorbrewer[colorbrewerScale] : undefined; - const palette: string[] = (currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]); - // Copy to avoid leaking mutations into the shared colorbrewer table by reference. - return palette.slice(); + const currentColorbrewer: IColorArray | undefined = colorbrewerScale ? colorbrewer[colorbrewerScale] : undefined; + const palette: string[] | undefined = (currentColorbrewer ? currentColorbrewer[numBuckets] : undefined) + ?? colorbrewer.Reds[numBuckets]; + if (palette && palette.length > 0) { + // Copy to avoid leaking mutations into the shared colorbrewer table by reference. + return palette.slice(); + } } const startColor: string = settingsModel.general.gradientStart.value.value; @@ -584,7 +539,7 @@ export class TableHeatMap implements IVisual { } private renderGrid(renderOptions: IRenderOptions): Selection { - const { chartData, colors, xOffset, yOffset, gridSizeHeight, gridSizeWidth } = renderOptions; + const { chartData, colors, xOffset, yOffset, gridSizeHeight, gridSizeWidth, settingsModel } = renderOptions; const grid = this.mainGraphics .append(TableHeatMap.HtmlObjG) @@ -608,7 +563,7 @@ export class TableHeatMap implements IVisual { .attr(TableHeatMap.AttrWidth, gridSizeWidth - TableHeatMap.ConstRectWidthAdjustment) .attr(TableHeatMap.AttrHeight, gridSizeHeight - TableHeatMap.ConstRectHeightAdjustment) .style(TableHeatMap.StFill, colors[0]) - .style("stroke", GeneralSettings.stroke); + .style("stroke", settingsModel.general.stroke); this.tooltipServiceWrapper.addTooltip(heatMap, (tooltipDataPoint: TooltipEnabledDataPoint) => { return tooltipDataPoint.tooltipInfo; @@ -685,7 +640,7 @@ export class TableHeatMap implements IVisual { .data(chartData.categoryY) .join(TableHeatMap.HtmlObjText) .text((d: powerbi.PrimitiveValue) => { - return TableHeatMap.textLimit(d.toString(), settingsModel.yAxisLabels.maxTextSymbol.value); + return textLimit(d.toString(), settingsModel.yAxisLabels.maxTextSymbol.value); }) .attr(TableHeatMap.AttrDY, TableHeatMap.Const071em) .attr(TableHeatMap.AttrX, TableHeatMap.Margin.left) @@ -805,7 +760,7 @@ export class TableHeatMap implements IVisual { .attr(TableHeatMap.AttrWidth, legendElementWidth - TableHeatMap.ConstRectWidthAdjustment) .attr(TableHeatMap.AttrHeight, gridSizeHeight - TableHeatMap.ConstRectHeightAdjustment) .style(TableHeatMap.StFill, (d) => colors[d.index]) - .style("stroke", GeneralSettings.stroke) + .style("stroke", settingsModel.general.stroke) .style("opacity", (d) => d.value !== maxDataValue ? 1 : 0) .classed(TableHeatMap.ClsBordered, true); @@ -871,14 +826,6 @@ export class TableHeatMap implements IVisual { this.behavior.renderSelection(); } - private static textLimit(text: string, limit: number) { - if (text.length > limit) { - return ((text || "").substring(0, limit).trim()) + "…"; - } - - return text; - } - private setSize(viewport: IViewport): void { this.svg .attr(TableHeatMap.AttrHeight, Math.max(viewport.height, 0)) diff --git a/stringResources/en-US/resources.resjson b/stringResources/en-US/resources.resjson index 41b9f8b..1358c78 100644 --- a/stringResources/en-US/resources.resjson +++ b/stringResources/en-US/resources.resjson @@ -1,4 +1,5 @@ { + "Visual_ActivateGradientMiddle": "Add gradient middle", "Visual_Category": "Category", "Visual_DataLabels": "Data labels", "Visual_Description_DisplayAllLabelsAnyway": "Display all labels anyway", @@ -7,9 +8,12 @@ "Visual_Font": "Font", "Visual_ForceDisplay": "Force display", "Visual_General": "General", + "Visual_General_Additional": "Additional settings", "Visual_General_Colorbrewer": "Colorbrewer", + "Visual_General_Gradient": "Gradient Colors", "Visual_General_Granularity": "Granularity", "Visual_GradientEnd": "Gradient end", + "Visual_GradientMiddle": "Gradient middle", "Visual_GradientStart": "Gradient start", "Visual_InvertColorScale": "Invert color scale", "Visual_LabelsFill": "Color", diff --git a/test/visualTest.ts b/test/visualTest.ts index 8f61752..416da35 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -1,862 +1,1122 @@ -/* -* Power BI Visualizations -* -* 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. -*/ - -import powerbi from "powerbi-visuals-api"; -import DataView = powerbi.DataView; -import { TableHeatMapBuilder } from "./visualBuilder"; -import { TableHeatMapData } from "./visualData"; -import { areColorsEqual } from "./helpers"; - -import { - pixelConverter as PixelConverter -} from "powerbi-visuals-utils-typeutils"; -import { - textMeasurementService as tms -} from "powerbi-visuals-utils-formattingutils"; - -import { TextProperties } from "powerbi-visuals-utils-formattingutils/lib/src/interfaces"; -import capabilities from '../capabilities.json'; -import { TableHeatMap } from "../src/visual"; -import { ClickEventType, d3Click, parseColorString, renderTimeout } from "powerbi-visuals-utils-testutils"; -import { getOpacity, DimmedOpacity, DefaultOpacity } from "../src/heatmapUtils"; - -const DefaultTimeout: number = 300; - -describe("TableHeatmap", () => { - let visualBuilder: TableHeatMapBuilder; - let dataView: DataView; - let defaultDataViewBuilder: TableHeatMapData; - - beforeEach(() => { - visualBuilder = new TableHeatMapBuilder(1000, 1000); - defaultDataViewBuilder = new TableHeatMapData(); - dataView = defaultDataViewBuilder.getDataView(); - }); - - afterEach(() => { - document.body.innerHTML = ""; - }) - - it("main DOM created", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - expect(visualBuilder.mainElement!).toBeTruthy(); - done(); - }, DefaultTimeout); - }); - - describe("short size", () => { - beforeEach(() => { - visualBuilder = new TableHeatMapBuilder(100, 100); - }); - - it("main DOM created", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - expect(visualBuilder.mainElement!).toBeTruthy(); - done(); - }, DefaultTimeout); - }); - }); - - describe("with objects", () => { - beforeEach(() => { - dataView.metadata.objects = { - general: { - colorbrewer: "YlGn", - buckets: 5, - } - }; - }); - - it("main DOM created", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - expect(visualBuilder.mainElement!).toBeTruthy(); - done(); - }, DefaultTimeout); - }); - }); - - it("data labels created", (done) => { - dataView.metadata.objects = { - labels: { - show: true - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - expect(document.querySelectorAll(".heatMapDataLabels")).toBeTruthy(); - done(); - }, DefaultTimeout); - }); - - it("data labels were not created", (done) => { - dataView.metadata.objects = { - labels: { - show: false - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - expect(document.querySelectorAll(".heatMapDataLabels").length).toBe(0); - done(); - }, DefaultTimeout); - }); - - describe("x axis label font", () => { - it("must resize", (done) => { - dataView.metadata.objects = { - xAxisLabels: { - show: true, - fontSize: 20 - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".categoryXLabel"); - const items = Array.from(labelDOMItems); - - const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "20px"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItem).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - - it("must resize", (done) => { - dataView.metadata.objects = { - xAxisLabels: { - show: true, - fontSize: 40 - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".categoryXLabel"); - const items = Array.from(labelDOMItems); - - const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "40px"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItem).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - - it("family must change", (done) => { - dataView.metadata.objects = { - xAxisLabels: { - show: true, - fontFamily: "Arial" - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".categoryXLabel"); - const items = Array.from(labelDOMItems); - - const filteredItem = items.find(i => getComputedStyle(i)["font-family"] === "Arial"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItem).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - }); - - describe("y axis label font", () => { - it("must resize", (done) => { - dataView.metadata.objects = { - yAxisLabels: { - show: true, - fontSize: 12 - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".categoryYLabel"); - const items = Array.from(labelDOMItems); - - const filteredItems = items.find(i => getComputedStyle(i)["font-size"] === "12px"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItems).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - - it("must resize", (done) => { - dataView.metadata.objects = { - yAxisLabels: { - show: true, - fontSize: 40 - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".categoryYLabel"); - const items = Array.from(labelDOMItems); - - const filteredItems = items.find(i => getComputedStyle(i)["font-size"] === "40px"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItems).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - - it("family must change", (done) => { - dataView.metadata.objects = { - yAxisLabels: { - show: true, - fontFamily: "Verdana" - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".categoryYLabel"); - const items = Array.from(labelDOMItems); - - const filteredItems = items.find(i => getComputedStyle(i)["font-family"] === "Verdana"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItems).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - }); - - describe("data label font", () => { - it("must resize", (done) => { - dataView.metadata.objects = { - labels: { - show: true, - fontSize: 24, - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".heatMapDataLabels"); - const items = Array.from(labelDOMItems); - - const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "24px"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItem).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - - it("must resize", (done) => { - dataView.metadata.objects = { - labels: { - show: true, - fontSize: 40, - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".heatMapDataLabels"); - const items = Array.from(labelDOMItems); - - const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "40px"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItem).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - - it("family must change", (done) => { - dataView.metadata.objects = { - labels: { - show: true, - fontFamily: "Verdana" - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let labelDOMItems = document.querySelectorAll(".heatMapDataLabels"); - const items = Array.from(labelDOMItems); - - const filteredItem = items.find(i => getComputedStyle(i)["font-family"] === "Verdana"); - - expect(labelDOMItems).toBeTruthy(); - expect(filteredItem).toBeTruthy(); - - done(); - }, DefaultTimeout); - }); - }); - - describe("data with null", () => { - it("must be transparent", (done) => { - dataView.metadata.objects = { - general: { - fillNullValuesCells: false - }, - labels: { - show: true, - fontSize: 12 - } - }; - - const valueColIndex: number = 2; - const transparentElementsCount: number = 2; - dataView.categorical!.values![0].values![valueColIndex] = ""; - dataView.categorical!.values![1].values![valueColIndex] = ""; - visualBuilder.updateRenderTimeout(dataView, () => { - let transparentElements: number = 0; - let rects = document.querySelectorAll("rect.categoryX"); - rects.forEach((el: Element) => { - if (+(getComputedStyle(el)["opacity"] || 1) === 0) { - transparentElements++; - } - }); - - expect(transparentElements).toBe(transparentElementsCount); - done(); - }, DefaultTimeout); - }); - - it("must be colored", (done) => { - dataView.metadata.objects = { - general: { - fillNullValuesCells: true - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let transparentElements: number = 0; - const transparentElementsCount: number = 0; - let rects = document.querySelectorAll("rect.categoryX"); - rects.forEach((el: Element) => { - if (+(getComputedStyle(el)["opacity"] || 1) === 0) { - transparentElements++; - } - }); - - expect(transparentElements).toBe(transparentElementsCount); - done(); - }, DefaultTimeout); - }); - }); - - describe("data with zero", () => { - it("must be 0 (not null)", (done) => { - dataView = defaultDataViewBuilder.getDataViewWithNullAndZero(); - dataView.metadata.objects = { - general: { - fillNullValuesCells: false - }, - labels: { - show: true - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let texts = document.querySelectorAll("text.categoryXLabel"); - let text: Element = texts[0]; - expect(text.textContent).toBe("0"); - done(); - }, DefaultTimeout); - }); - }); - - describe("cell size", () => { - it("must resize with big font size of cell data labels", (done) => { - const fontSize: number = 40; - const fontFamily: string = "Arial"; - dataView.metadata.objects = { - labels: { - show: true, - fontFamily: fontFamily, - fontSize: fontSize - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - let textProperties: TextProperties = { - fontSize: PixelConverter.toString(fontSize), - fontFamily: fontFamily, - text: "00" - }; - let textRect: SVGRect = tms.measureSvgTextRect(textProperties); - expect(+document.querySelector(".categoryX")!.getAttribute("width")!).toBeGreaterThan(textRect.width); - done(); - }, DefaultTimeout); - }); - - it("height must be limited", (done) => { - dataView = defaultDataViewBuilder.getDataViewWithOneCategory(); - visualBuilder.updateRenderTimeout(dataView, () => { - const cellMaxHeightLimit: number = TableHeatMap.CellMaxHeightLimit; - expect(+document.querySelector(".categoryX")!.getAttribute("height")!).toBeLessThanOrEqual(cellMaxHeightLimit); - done(); - }, DefaultTimeout); - }); - }); - - describe("Capabilities tests", () => { - it("all items having displayName should have displayNameKey property", () => { - let objectsChecker: Function = (obj) => { - for (let property in obj) { - let value: any = obj[property]; - - if (property === "enumeration") { - continue; - } - - if (value.displayName) { - expect(value.displayNameKey).toBeDefined(); - } - - if (typeof value === "object") { - objectsChecker(value); - } - } - }; - - objectsChecker(capabilities.objects); - }); - - describe("Accessibility", () => { - describe("High contrast mode", () => { - const backgroundColor: string = "#000000"; - const foregroundColor: string = "#ffff00"; - - beforeEach(() => { - visualBuilder.visualHost.colorPalette.isHighContrast = true; - - visualBuilder.visualHost.colorPalette.background = { value: backgroundColor }; - visualBuilder.visualHost.colorPalette.foreground = { value: foregroundColor }; - }); - - it("should use background theme color as fill", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - const rects = Array.from(visualBuilder.rects!); - - expect(isColorAppliedToElements(rects, backgroundColor, "fill")); - - done(); - }); - }); - - it("should use foreground theme color as stroke", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - const rects = Array.from(visualBuilder.rects!); - - expect(isColorAppliedToElements(rects, foregroundColor, "stroke")); - - done(); - }); - }); - - function isColorAppliedToElements( - elements: Element[], - color?: string, - colorStyleName: string = "fill" - ): boolean { - return elements.some((element: Element) => { - const currentColor: string = getComputedStyle(element)[colorStyleName]; - - if (!currentColor || !color) { - return currentColor === color; - } - - return areColorsEqual(currentColor, color); - }); - } - }); - }); - }); - describe("Selection tests", () => { - beforeEach(() => { - dataView = defaultDataViewBuilder.getDataViewWithSeries(); - }); - - it("element can be selected", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - const firstRect = visualBuilder.rects![0]; - d3Click(firstRect, 0, 0, ClickEventType.Default); - - renderTimeout(() => { - expect(visualBuilder.selectedRects?.length).toBe(1); - done(); - }); - }); - }); - - it("element can be deselected", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - const firstRect = visualBuilder.rects![0]; - d3Click(firstRect, 0, 0, ClickEventType.Default); - - renderTimeout(() => { - expect(visualBuilder.selectedRects?.length).toBe(1); - d3Click(firstRect, 0, 0, ClickEventType.CtrlKey); - - renderTimeout(() => { - expect(visualBuilder.selectedRects?.length).toBe(0); - done(); - }); - }); - }); - }); - - it("multi-selection should work with ctrlKey", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - checkMultiselection(ClickEventType.CtrlKey, done); - }); - }); - - it("multi-selection should work with metaKey", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - checkMultiselection(ClickEventType.MetaKey, done); - }); - }); - - it("multi-selection should work with shiftKey", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - checkMultiselection(ClickEventType.ShiftKey, done); - }); - }); - - function checkMultiselection(eventType: number, done: DoneFn): void { - const firstColumn = visualBuilder.rects![0]; - const secondColumn = visualBuilder.rects![1]; - d3Click(firstColumn, 0, 0, ClickEventType.Default); - renderTimeout(() => { - expect(visualBuilder.selectedRects?.length).toBe(1); - - d3Click(secondColumn, 0, 0, eventType); - - renderTimeout(() => { - expect(visualBuilder.selectedRects?.length).toBe(2); - done(); - }); - }); - } - }); - - describe("Keyboard navigation and related aria-attributes tests:", () => { - beforeEach(() => { - dataView = defaultDataViewBuilder.getDataViewWithSeries(); - }); - - it("should have role=grid and aria-multiselectable attributes correctly set", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - const grid = visualBuilder.grid; - - expect(grid!.getAttribute("role")).toBe("grid"); - expect(grid!.getAttribute("aria-multiselectable")).toBe("true"); - - done(); - }); - }); - - it("should have role=presentation correctly set on text labels", (done) => { - visualBuilder.updateRenderTimeout(dataView, () => { - - const labels = Array.from(visualBuilder.labels!); - for (const label of labels) { - expect(label.getAttribute("role")).toBe("presentation"); - } - - done(); - }); - }); - - it("enter toggles the correct column", (done) => { - const enterEvent = new KeyboardEvent("keydown", { key: "enter", code: "Enter", bubbles: true }); - checkKeyboardSingleSelection(enterEvent, done); - }); - - it("space toggles the correct column", (done) => { - const spaceEvent = new KeyboardEvent("keydown", { code: "Space", bubbles: true }); - checkKeyboardSingleSelection(spaceEvent, done); - }); - - it("multiselection should work with ctrlKey", (done) => { - const enterEventCtrlKey = new KeyboardEvent("keydown", { code: "Enter", bubbles: true, ctrlKey: true }); - checkKeyboardMultiSelection(enterEventCtrlKey, done); - }); - - it("multiselection should work with metaKey", (done) => { - const enterEventMetaKey = new KeyboardEvent("keydown", { code: "Enter", bubbles: true, metaKey: true }); - checkKeyboardMultiSelection(enterEventMetaKey, done); - }); - - it("multiselection should work with shiftKey", (done) => { - const enterEventShiftKey = new KeyboardEvent("keydown", { code: "Enter", bubbles: true, shiftKey: true }); - checkKeyboardMultiSelection(enterEventShiftKey, done); - }); - - it("element can be focused", () => { - visualBuilder.updateFlushAllD3Transitions(dataView); - - const rects = Array.from(visualBuilder.rects!); - const firstRect = rects[0]; - - rects.forEach((rect) => { - expect(rect.matches(":focus-visible")).toBeFalse(); - }); - - firstRect.focus(); - expect(firstRect.matches(':focus-visible')).toBeTrue(); - - const otherRects = rects.slice(1); - otherRects.forEach((rect) => { - expect(rect.matches(":focus-visible")).toBeFalse(); - }); - - }); - - function checkKeyboardSingleSelection(keyboardSingleSelectionEvent: KeyboardEvent, done: DoneFn): void { - visualBuilder.updateFlushAllD3Transitions(dataView); - let rects = Array.from(visualBuilder.rects!); - const firstRect = rects[0]; - const secondRect = rects[1]; - - firstRect.dispatchEvent(keyboardSingleSelectionEvent); - renderTimeout(() => { - expect(firstRect.getAttribute("aria-selected")).toBe("true"); - const otherRects = rects.slice(1); - otherRects.forEach((rect) => { - expect(rect.getAttribute("aria-selected")).toBe("false"); - }); - - secondRect.dispatchEvent(keyboardSingleSelectionEvent); - renderTimeout(() => { - expect(secondRect.getAttribute("aria-selected")).toBe("true"); - - rects.splice(1, 1); - rects.forEach((rect) => { - expect(rect.getAttribute("aria-selected")).toBe("false"); - }); - done(); - }); - }); - } - - function checkKeyboardMultiSelection(keyboardMultiselectionEvent: KeyboardEvent, done: DoneFn): void { - visualBuilder.updateFlushAllD3Transitions(dataView); - const enterEvent = new KeyboardEvent("keydown", { code: "Enter", bubbles: true }); - const rects = Array.from(visualBuilder.rects!); - const firstRect = rects[0]; - const secondRect = rects[1]; - - // select first column - firstRect.dispatchEvent(enterEvent); - // multiselect second column - secondRect.dispatchEvent(keyboardMultiselectionEvent); - renderTimeout(() => { - expect(firstRect.getAttribute("aria-selected")).toBe("true"); - expect(secondRect.getAttribute("aria-selected")).toBe("true"); - expect(visualBuilder.selectedRects?.length).toBe(2); - done(); - }); - } - }); - - describe("invertColorScale", () => { - // The visual animates cell fills via a d3 transition of 1000ms - // (see TableHeatMap.animationDuration). Tests must wait longer than that - // to read final fills; flushAllD3Transitions does not help because the - // visual and test-utils carry separate d3-timer instances. - const AnimationTimeout: number = 1200; - - beforeEach(() => { - dataView = defaultDataViewBuilder.getDataViewWithSeries(); - }); - - const getCellFills = (): string[] => - Array.from(document.querySelectorAll("rect.categoryX")) - .map((el: Element) => getComputedStyle(el)["fill"]); - - // Normalizes a CSS color to a stable key for use in maps/sets. - const colorKey = (color: string): string => { - const { R, G, B } = parseColorString(color); - return `${R},${G},${B}`; - }; - - // Asserts that `invertedFills` is the result of palette inversion applied - // to the same data: i.e. cells are re-colored in a consistent permutation, - // and the permutation is non-trivial. - // We do NOT require the set of colors to be equal, because data may not - // hit every palette bucket — and the buckets used in normal vs inverted - // are mirrored positions, which can be different subsets of the palette. - const expectPaletteReversed = (normalFills: string[], invertedFills: string[]): void => { - expect(invertedFills.length).toBe(normalFills.length); - - // Consistent permutation: cells that share the same color before inversion - // must also share the same color after inversion. This is the defining - // property of "the same value mapped through a re-ordered palette". - const mapping = new Map(); - normalFills.forEach((n, i) => { - const nKey = colorKey(n); - const iKey = colorKey(invertedFills[i]); - const existing = mapping.get(nKey); - if (existing === undefined) { - mapping.set(nKey, iKey); - } else { - expect(iKey).toBe(existing); - } - }); - - // Non-trivial: at least one cell must actually change color. - const changedCount = normalFills.filter((fill, i) => !areColorsEqual(fill, invertedFills[i])).length; - expect(changedCount).toBeGreaterThan(0); - }; - - // Renders the visual twice (invert off, then invert on) and runs the - // assertion against the resulting fill arrays. Waits long enough for the - // d3 fill animation to finish so reads pick up final colors. - const renderAndCompare = ( - baseGeneral: Record, - assertion: (normal: string[], inverted: string[]) => void, - done: DoneFn - ): void => { - dataView.metadata.objects = { general: { ...baseGeneral, invertColorScale: false } }; - visualBuilder.updateRenderTimeout(dataView, () => { - const normalFills = getCellFills(); - - dataView.metadata.objects = { general: { ...baseGeneral, invertColorScale: true } }; - visualBuilder.updateRenderTimeout(dataView, () => { - assertion(normalFills, getCellFills()); - done(); - }, AnimationTimeout); - }, AnimationTimeout); - }; - - it("should reverse the colorbrewer palette as an involution", (done) => { - renderAndCompare( - { enableColorbrewer: true, colorbrewer: "Reds", buckets: 5 }, - expectPaletteReversed, - done - ); - }); - - it("should reverse the custom gradient palette as an involution", (done) => { - renderAndCompare( - { - enableColorbrewer: false, - gradientStart: { solid: { color: "#0000FF" } }, - gradientEnd: { solid: { color: "#FF0000" } } - }, - expectPaletteReversed, - done - ); - }); - - // Helper to read gradient pickers as they currently exist in the visual's settings - // model (i.e. what the user would see in the formatting pane). - const readGradientPickers = (): { start: string; end: string } => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const general = (visualBuilder as any).visual.settingsModel.general; - return { - start: general.gradientStart.value.value, - end: general.gradientEnd.value.value - }; - }; - - it("should NOT mutate user gradient pickers when invert is toggled in custom gradient mode", (done) => { - const userStart = "#0000FF"; - const userEnd = "#FF0000"; - const base = { - enableColorbrewer: false, - gradientStart: { solid: { color: userStart } }, - gradientEnd: { solid: { color: userEnd } } - }; - - dataView.metadata.objects = { general: { ...base, invertColorScale: false } }; - visualBuilder.updateRenderTimeout(dataView, () => { - const initial = readGradientPickers(); - expect(areColorsEqual(initial.start, userStart)).toBeTrue(); - expect(areColorsEqual(initial.end, userEnd)).toBeTrue(); - - dataView.metadata.objects = { general: { ...base, invertColorScale: true } }; - visualBuilder.updateRenderTimeout(dataView, () => { - // Pickers must reflect the user's original choices, NOT the swapped colors. - const afterInvert = readGradientPickers(); - expect(areColorsEqual(afterInvert.start, userStart)).toBeTrue(); - expect(areColorsEqual(afterInvert.end, userEnd)).toBeTrue(); - done(); - }, AnimationTimeout); - }, AnimationTimeout); - }); - - it("should keep gradient pickers in sync with the base (non-inverted) palette in colorbrewer mode", (done) => { - const base = { enableColorbrewer: true, colorbrewer: "Reds", buckets: 5 }; - - dataView.metadata.objects = { general: { ...base, invertColorScale: false } }; - visualBuilder.updateRenderTimeout(dataView, () => { - const baseline = readGradientPickers(); - - dataView.metadata.objects = { general: { ...base, invertColorScale: true } }; - visualBuilder.updateRenderTimeout(dataView, () => { - // Pickers must show the SAME endpoints as in the non-inverted render — - // they preview the base palette so the user has predictable defaults - // when switching to custom gradient mode. - const afterInvert = readGradientPickers(); - expect(areColorsEqual(afterInvert.start, baseline.start)).toBeTrue(); - expect(areColorsEqual(afterInvert.end, baseline.end)).toBeTrue(); - done(); - }, AnimationTimeout); - }, AnimationTimeout); - }); - }); - - describe("utils:getOpacity", () => { - it("returns DefaultOpacity when no selection or highlights are active", () => { - expect(getOpacity(false, false, false, false)).toBe(DefaultOpacity); - }); - - it("returns DefaultOpacity for a selected element when selection is active", () => { - expect(getOpacity(true, false, true, false)).toBe(DefaultOpacity); - }); - - it("returns DimmedOpacity for an unselected element when selection is active", () => { - expect(getOpacity(false, false, true, false)).toBe(DimmedOpacity); - }); - - it("returns DefaultOpacity for a highlighted element when partial highlights are active", () => { - expect(getOpacity(false, true, false, true)).toBe(DefaultOpacity); - }); - - it("returns DimmedOpacity for a non-highlighted element when partial highlights are active", () => { - expect(getOpacity(false, false, false, true)).toBe(DimmedOpacity); - }); - }); -}); - +/* +* Power BI Visualizations +* +* 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. +*/ + +import powerbi from "powerbi-visuals-api"; +import DataView = powerbi.DataView; +import { TableHeatMapBuilder } from "./visualBuilder"; +import { TableHeatMapData } from "./visualData"; +import { areColorsEqual } from "./helpers"; + +import { + pixelConverter as PixelConverter +} from "powerbi-visuals-utils-typeutils"; +import { + textMeasurementService as tms +} from "powerbi-visuals-utils-formattingutils"; + +import { TextProperties } from "powerbi-visuals-utils-formattingutils/lib/src/interfaces"; +import capabilities from '../capabilities.json'; +import { TableHeatMap } from "../src/visual"; +import { ClickEventType, createColorPalette, d3Click, parseColorString, renderTimeout } from "powerbi-visuals-utils-testutils"; +import { ColorHelper } from "powerbi-visuals-utils-colorutils"; +import { TableHeatMapChartData } from "../src/dataInterfaces"; +import { colorbrewer, SettingsModel } from "../src/settings"; +import { + getOpacity, DimmedOpacity, DefaultOpacity, DimmedColor, + isDataViewValid, textLimit, + calculateGridSizeHeight, calculateGridSizeWidth, + ConstGridMinHeight, CellMaxHeightLimit, ConstGridMinWidth, CellMaxWidthFactorLimit, + getYAxisWidth, getXAxisHeight, getYAxisHeight, + parseSettings +} from "../src/heatmapUtils"; + +const DefaultTimeout: number = 300; +const AnimationTimeout: number = 1200; + +function isColorAppliedToElements( + elements: Element[], + color?: string, + colorStyleName: string = "fill" +): boolean { + return elements.some((element: Element) => { + const currentColor: string = getComputedStyle(element)[colorStyleName]; + + if (!currentColor || !color) { + return currentColor === color; + } + + return areColorsEqual(currentColor, color); + }); +} + +const getCellFills = (): string[] => + Array.from(document.querySelectorAll("rect.categoryX")) + .map((el: Element) => getComputedStyle(el)["fill"]); + +const colorKey = (color: string): string => { + const { R, G, B } = parseColorString(color); + return `${R},${G},${B}`; +}; + +describe("TableHeatmap", () => { + let visualBuilder: TableHeatMapBuilder; + let dataView: DataView; + let defaultDataViewBuilder: TableHeatMapData; + + beforeEach(() => { + visualBuilder = new TableHeatMapBuilder(1000, 1000); + defaultDataViewBuilder = new TableHeatMapData(); + dataView = defaultDataViewBuilder.getDataView(); + }); + + afterEach(() => { + document.body.innerHTML = ""; + }) + + it("main DOM created", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + expect(visualBuilder.mainElement!).toBeTruthy(); + done(); + }, DefaultTimeout); + }); + + describe("short size", () => { + beforeEach(() => { + visualBuilder = new TableHeatMapBuilder(100, 100); + }); + + it("renders under short viewport", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + expect(visualBuilder.mainElement!).toBeTruthy(); + done(); + }, DefaultTimeout); + }); + }); + + describe("with objects", () => { + beforeEach(() => { + dataView.metadata.objects = { + general: { + colorbrewer: "YlGn", + buckets: 5, + } + }; + }); + + it("renders with colorbrewer objects", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + expect(visualBuilder.mainElement!).toBeTruthy(); + done(); + }, DefaultTimeout); + }); + }); + + it("data labels created", (done) => { + dataView.metadata.objects = { + labels: { + show: true + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + expect(document.querySelectorAll(".heatMapDataLabels").length).toBeGreaterThan(0); + done(); + }, DefaultTimeout); + }); + + it("data labels were not created", (done) => { + dataView.metadata.objects = { + labels: { + show: false + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + expect(document.querySelectorAll(".heatMapDataLabels").length).toBe(0); + done(); + }, DefaultTimeout); + }); + + describe("x axis label font", () => { + it("must resize to 20px", (done) => { + dataView.metadata.objects = { + xAxisLabels: { + show: true, + fontSize: 20 + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".categoryXLabel"); + const items = Array.from(labelDOMItems); + + const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "20px"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItem).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + + it("must resize to 40px", (done) => { + dataView.metadata.objects = { + xAxisLabels: { + show: true, + fontSize: 40 + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".categoryXLabel"); + const items = Array.from(labelDOMItems); + + const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "40px"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItem).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + + it("family must change", (done) => { + dataView.metadata.objects = { + xAxisLabels: { + show: true, + fontFamily: "Arial" + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".categoryXLabel"); + const items = Array.from(labelDOMItems); + + const filteredItem = items.find(i => getComputedStyle(i)["font-family"] === "Arial"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItem).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + }); + + describe("y axis label font", () => { + it("must resize to 12px", (done) => { + dataView.metadata.objects = { + yAxisLabels: { + show: true, + fontSize: 12 + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".categoryYLabel"); + const items = Array.from(labelDOMItems); + + const filteredItems = items.find(i => getComputedStyle(i)["font-size"] === "12px"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItems).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + + it("must resize to 40px", (done) => { + dataView.metadata.objects = { + yAxisLabels: { + show: true, + fontSize: 40 + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".categoryYLabel"); + const items = Array.from(labelDOMItems); + + const filteredItems = items.find(i => getComputedStyle(i)["font-size"] === "40px"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItems).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + + it("family must change", (done) => { + dataView.metadata.objects = { + yAxisLabels: { + show: true, + fontFamily: "Verdana" + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".categoryYLabel"); + const items = Array.from(labelDOMItems); + + const filteredItems = items.find(i => getComputedStyle(i)["font-family"] === "Verdana"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItems).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + }); + + describe("data label font", () => { + it("must resize to 24px", (done) => { + dataView.metadata.objects = { + labels: { + show: true, + fontSize: 24, + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".heatMapDataLabels"); + const items = Array.from(labelDOMItems); + + const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "24px"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItem).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + + it("must resize to 40px", (done) => { + dataView.metadata.objects = { + labels: { + show: true, + fontSize: 40, + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".heatMapDataLabels"); + const items = Array.from(labelDOMItems); + + const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "40px"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItem).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + + it("family must change", (done) => { + dataView.metadata.objects = { + labels: { + show: true, + fontFamily: "Verdana" + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let labelDOMItems = document.querySelectorAll(".heatMapDataLabels"); + const items = Array.from(labelDOMItems); + + const filteredItem = items.find(i => getComputedStyle(i)["font-family"] === "Verdana"); + + expect(labelDOMItems.length).toBeGreaterThan(0); + expect(filteredItem).toBeTruthy(); + + done(); + }, DefaultTimeout); + }); + }); + + describe("data with null", () => { + it("must be transparent", (done) => { + dataView.metadata.objects = { + general: { + fillNullValuesCells: false + }, + labels: { + show: true, + fontSize: 12 + } + }; + + const valueColIndex: number = 2; + const transparentElementsCount: number = 2; + dataView.categorical!.values![0].values![valueColIndex] = ""; + dataView.categorical!.values![1].values![valueColIndex] = ""; + visualBuilder.updateRenderTimeout(dataView, () => { + let transparentElements: number = 0; + let rects = document.querySelectorAll("rect.categoryX"); + rects.forEach((el: Element) => { + if (+(getComputedStyle(el)["opacity"] || 1) === 0) { + transparentElements++; + } + }); + + expect(transparentElements).toBe(transparentElementsCount); + done(); + }, DefaultTimeout); + }); + + it("must be colored", (done) => { + dataView.metadata.objects = { + general: { + fillNullValuesCells: true + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let transparentElements: number = 0; + const transparentElementsCount: number = 0; + let rects = document.querySelectorAll("rect.categoryX"); + rects.forEach((el: Element) => { + if (+(getComputedStyle(el)["opacity"] || 1) === 0) { + transparentElements++; + } + }); + + expect(transparentElements).toBe(transparentElementsCount); + done(); + }, DefaultTimeout); + }); + }); + + describe("data with zero", () => { + it("must be 0 (not null)", (done) => { + dataView = defaultDataViewBuilder.getDataViewWithNullAndZero(); + dataView.metadata.objects = { + general: { + fillNullValuesCells: false + }, + labels: { + show: true + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let texts = document.querySelectorAll("text.categoryXLabel"); + let text: Element = texts[0]; + expect(text.textContent).toBe("0"); + done(); + }, DefaultTimeout); + }); + }); + + describe("cell size", () => { + it("must resize with big font size of cell data labels", (done) => { + const fontSize: number = 40; + const fontFamily: string = "Arial"; + dataView.metadata.objects = { + labels: { + show: true, + fontFamily: fontFamily, + fontSize: fontSize + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + let textProperties: TextProperties = { + fontSize: PixelConverter.toString(fontSize), + fontFamily: fontFamily, + text: "00" + }; + let textRect: SVGRect = tms.measureSvgTextRect(textProperties); + expect(+document.querySelector(".categoryX")!.getAttribute("width")!).toBeGreaterThan(textRect.width); + done(); + }, DefaultTimeout); + }); + + it("height must be limited", (done) => { + dataView = defaultDataViewBuilder.getDataViewWithOneCategory(); + visualBuilder.updateRenderTimeout(dataView, () => { + const cellMaxHeightLimit: number = TableHeatMap.CellMaxHeightLimit; + expect(+document.querySelector(".categoryX")!.getAttribute("height")!).toBeLessThanOrEqual(cellMaxHeightLimit); + done(); + }, DefaultTimeout); + }); + }); + + describe("Capabilities tests", () => { + it("all items having displayName should have displayNameKey property", () => { + let objectsChecker: Function = (obj) => { + for (let property in obj) { + let value: any = obj[property]; + + if (property === "enumeration") { + continue; + } + + if (value.displayName) { + expect(value.displayNameKey).toBeDefined(); + } + + if (typeof value === "object") { + objectsChecker(value); + } + } + }; + + objectsChecker(capabilities.objects); + }); + + describe("Accessibility", () => { + describe("High contrast mode", () => { + const backgroundColor: string = "#000000"; + const foregroundColor: string = "#ffff00"; + + beforeEach(() => { + visualBuilder.visualHost.colorPalette.isHighContrast = true; + + visualBuilder.visualHost.colorPalette.background = { value: backgroundColor }; + visualBuilder.visualHost.colorPalette.foreground = { value: foregroundColor }; + }); + + it("should use background theme color as fill", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + const rects = Array.from(visualBuilder.rects!); + expect(isColorAppliedToElements(rects, backgroundColor, "fill")).toBeTrue(); + done(); + }, DefaultTimeout); + }); + + it("should use foreground theme color as stroke", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + const rects = Array.from(visualBuilder.rects!); + expect(isColorAppliedToElements(rects, foregroundColor, "stroke")).toBeTrue(); + done(); + }, DefaultTimeout); + }); + + }); + }); + }); + describe("Selection tests", () => { + beforeEach(() => { + dataView = defaultDataViewBuilder.getDataViewWithSeries(); + }); + + it("element can be selected", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + const firstRect = visualBuilder.rects![0]; + d3Click(firstRect, 0, 0, ClickEventType.Default); + + renderTimeout(() => { + expect(visualBuilder.selectedRects?.length).toBe(1); + done(); + }); + }); + }); + + it("element can be deselected", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + const firstRect = visualBuilder.rects![0]; + d3Click(firstRect, 0, 0, ClickEventType.Default); + + renderTimeout(() => { + expect(visualBuilder.selectedRects?.length).toBe(1); + d3Click(firstRect, 0, 0, ClickEventType.CtrlKey); + + renderTimeout(() => { + expect(visualBuilder.selectedRects?.length).toBe(0); + done(); + }); + }); + }); + }); + + it("multi-selection should work with ctrlKey", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + checkMultiselection(ClickEventType.CtrlKey, done); + }); + }); + + it("multi-selection should work with metaKey", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + checkMultiselection(ClickEventType.MetaKey, done); + }); + }); + + it("multi-selection should work with shiftKey", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + checkMultiselection(ClickEventType.ShiftKey, done); + }); + }); + + function checkMultiselection(eventType: number, done: DoneFn): void { + const firstColumn = visualBuilder.rects![0]; + const secondColumn = visualBuilder.rects![1]; + d3Click(firstColumn, 0, 0, ClickEventType.Default); + renderTimeout(() => { + expect(visualBuilder.selectedRects?.length).toBe(1); + + d3Click(secondColumn, 0, 0, eventType); + + renderTimeout(() => { + expect(visualBuilder.selectedRects?.length).toBe(2); + done(); + }); + }); + } + }); + + describe("Keyboard navigation and related aria-attributes tests:", () => { + beforeEach(() => { + dataView = defaultDataViewBuilder.getDataViewWithSeries(); + }); + + it("should have role=grid and aria-multiselectable attributes correctly set", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + const grid = visualBuilder.grid; + + expect(grid!.getAttribute("role")).toBe("grid"); + expect(grid!.getAttribute("aria-multiselectable")).toBe("true"); + + done(); + }); + }); + + it("should have role=presentation correctly set on text labels", (done) => { + visualBuilder.updateRenderTimeout(dataView, () => { + + const labels = Array.from(visualBuilder.labels!); + for (const label of labels) { + expect(label.getAttribute("role")).toBe("presentation"); + } + + done(); + }); + }); + + it("enter toggles the correct column", (done) => { + const enterEvent = new KeyboardEvent("keydown", { key: "enter", code: "Enter", bubbles: true }); + checkKeyboardSingleSelection(enterEvent, done); + }); + + it("space toggles the correct column", (done) => { + const spaceEvent = new KeyboardEvent("keydown", { code: "Space", bubbles: true }); + checkKeyboardSingleSelection(spaceEvent, done); + }); + + it("multiselection should work with ctrlKey", (done) => { + const enterEventCtrlKey = new KeyboardEvent("keydown", { code: "Enter", bubbles: true, ctrlKey: true }); + checkKeyboardMultiSelection(enterEventCtrlKey, done); + }); + + it("multiselection should work with metaKey", (done) => { + const enterEventMetaKey = new KeyboardEvent("keydown", { code: "Enter", bubbles: true, metaKey: true }); + checkKeyboardMultiSelection(enterEventMetaKey, done); + }); + + it("multiselection should work with shiftKey", (done) => { + const enterEventShiftKey = new KeyboardEvent("keydown", { code: "Enter", bubbles: true, shiftKey: true }); + checkKeyboardMultiSelection(enterEventShiftKey, done); + }); + + it("element can be focused", () => { + visualBuilder.updateFlushAllD3Transitions(dataView); + + const rects = Array.from(visualBuilder.rects!); + const firstRect = rects[0]; + + rects.forEach((rect) => { + expect(rect.matches(":focus-visible")).toBeFalse(); + }); + + firstRect.focus(); + expect(firstRect.matches(':focus-visible')).toBeTrue(); + + const otherRects = rects.slice(1); + otherRects.forEach((rect) => { + expect(rect.matches(":focus-visible")).toBeFalse(); + }); + + }); + + function checkKeyboardSingleSelection(keyboardSingleSelectionEvent: KeyboardEvent, done: DoneFn): void { + visualBuilder.updateFlushAllD3Transitions(dataView); + let rects = Array.from(visualBuilder.rects!); + const firstRect = rects[0]; + const secondRect = rects[1]; + + firstRect.dispatchEvent(keyboardSingleSelectionEvent); + renderTimeout(() => { + expect(firstRect.getAttribute("aria-selected")).toBe("true"); + const otherRects = rects.slice(1); + otherRects.forEach((rect) => { + expect(rect.getAttribute("aria-selected")).toBe("false"); + }); + + secondRect.dispatchEvent(keyboardSingleSelectionEvent); + renderTimeout(() => { + expect(secondRect.getAttribute("aria-selected")).toBe("true"); + + rects.splice(1, 1); + rects.forEach((rect) => { + expect(rect.getAttribute("aria-selected")).toBe("false"); + }); + done(); + }); + }); + } + + function checkKeyboardMultiSelection(keyboardMultiselectionEvent: KeyboardEvent, done: DoneFn): void { + visualBuilder.updateFlushAllD3Transitions(dataView); + const enterEvent = new KeyboardEvent("keydown", { code: "Enter", bubbles: true }); + const rects = Array.from(visualBuilder.rects!); + const firstRect = rects[0]; + const secondRect = rects[1]; + + // select first column + firstRect.dispatchEvent(enterEvent); + // multiselect second column + secondRect.dispatchEvent(keyboardMultiselectionEvent); + renderTimeout(() => { + expect(firstRect.getAttribute("aria-selected")).toBe("true"); + expect(secondRect.getAttribute("aria-selected")).toBe("true"); + expect(visualBuilder.selectedRects?.length).toBe(2); + done(); + }); + } + }); + + // Asserts that `invertedFills` is the result of palette inversion applied + // to the same data: i.e. cells are re-colored in a consistent permutation, + // and the permutation is non-trivial. + // We do NOT require the set of colors to be equal, because data may not + // hit every palette bucket — and the buckets used in normal vs inverted + // are mirrored positions, which can be different subsets of the palette. + const expectPaletteReversed = (normalFills: string[], invertedFills: string[]): void => { + expect(invertedFills.length).toBe(normalFills.length); + + // Consistent permutation: cells that share the same color before inversion + // must also share the same color after inversion. This is the defining + // property of "the same value mapped through a re-ordered palette". + const mapping = new Map(); + normalFills.forEach((n, i) => { + const nKey = colorKey(n); + const iKey = colorKey(invertedFills[i]); + const existing = mapping.get(nKey); + if (existing === undefined) { + mapping.set(nKey, iKey); + } else { + expect(iKey).toBe(existing); + } + }); + + // Non-trivial: at least one cell must actually change color. + const changedCount = normalFills.filter((fill, i) => !areColorsEqual(fill, invertedFills[i])).length; + expect(changedCount).toBeGreaterThan(0); + }; + + describe("invertColorScale", () => { + // The visual animates cell fills via a d3 transition of 1000ms + // (see TableHeatMap.animationDuration). Tests must wait longer than that + // to read final fills; flushAllD3Transitions does not help because the + // visual and test-utils carry separate d3-timer instances. + + beforeEach(() => { + dataView = defaultDataViewBuilder.getDataViewWithSeries(); + }); + + // Renders the visual twice (invert off, then invert on) and runs the + // assertion against the resulting fill arrays. Waits long enough for the + // d3 fill animation to finish so reads pick up final colors. + const renderAndCompare = ( + baseGeneral: Record, + assertion: (normal: string[], inverted: string[]) => void, + done: DoneFn + ): void => { + dataView.metadata.objects = { general: { ...baseGeneral, invertColorScale: false } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const normalFills = getCellFills(); + + dataView.metadata.objects = { general: { ...baseGeneral, invertColorScale: true } }; + visualBuilder.updateRenderTimeout(dataView, () => { + assertion(normalFills, getCellFills()); + done(); + }, AnimationTimeout); + }, AnimationTimeout); + }; + + it("should reverse the colorbrewer palette as an involution", (done) => { + renderAndCompare( + { enableColorbrewer: true, colorbrewer: "Reds", buckets: 5 }, + expectPaletteReversed, + done + ); + }); + + it("should reverse the custom gradient palette as an involution", (done) => { + renderAndCompare( + { + enableColorbrewer: false, + gradientStart: { solid: { color: "#0000FF" } }, + gradientEnd: { solid: { color: "#FF0000" } } + }, + expectPaletteReversed, + done + ); + }); + + // Helper to read gradient pickers as they currently exist in the visual's settings + // model (i.e. what the user would see in the formatting pane). + const readGradientPickers = (): { start: string; end: string } => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const general = (visualBuilder as any).visual.settingsModel.general; + return { + start: general.gradientStart.value.value, + end: general.gradientEnd.value.value + }; + }; + + it("should NOT mutate user gradient pickers when invert is toggled in custom gradient mode", (done) => { + const userStart = "#0000FF"; + const userEnd = "#FF0000"; + const base = { + enableColorbrewer: false, + gradientStart: { solid: { color: userStart } }, + gradientEnd: { solid: { color: userEnd } } + }; + + dataView.metadata.objects = { general: { ...base, invertColorScale: false } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const initial = readGradientPickers(); + expect(areColorsEqual(initial.start, userStart)).toBeTrue(); + expect(areColorsEqual(initial.end, userEnd)).toBeTrue(); + + dataView.metadata.objects = { general: { ...base, invertColorScale: true } }; + visualBuilder.updateRenderTimeout(dataView, () => { + // Pickers must reflect the user's original choices, NOT the swapped colors. + const afterInvert = readGradientPickers(); + expect(areColorsEqual(afterInvert.start, userStart)).toBeTrue(); + expect(areColorsEqual(afterInvert.end, userEnd)).toBeTrue(); + done(); + }, AnimationTimeout); + }, AnimationTimeout); + }); + + it("should keep gradient pickers in sync with the base (non-inverted) palette in colorbrewer mode", (done) => { + const base = { enableColorbrewer: true, colorbrewer: "Reds", buckets: 5 }; + + dataView.metadata.objects = { general: { ...base, invertColorScale: false } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const baseline = readGradientPickers(); + + dataView.metadata.objects = { general: { ...base, invertColorScale: true } }; + visualBuilder.updateRenderTimeout(dataView, () => { + // Pickers must show the SAME endpoints as in the non-inverted render — + // they preview the base palette so the user has predictable defaults + // when switching to custom gradient mode. + const afterInvert = readGradientPickers(); + expect(areColorsEqual(afterInvert.start, baseline.start)).toBeTrue(); + expect(areColorsEqual(afterInvert.end, baseline.end)).toBeTrue(); + done(); + }, AnimationTimeout); + }, AnimationTimeout); + }); + }); + + describe("activateGradientMiddle", () => { + beforeEach(() => { + dataView = defaultDataViewBuilder.getDataViewWithSeries(); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const readMiddlePicker = (): string => (visualBuilder as any).visual.settingsModel.general.gradientMiddle.value.value; + + it("middle picker retains a valid color when activateGradientMiddle is first enabled in custom gradient mode", (done) => { + dataView.metadata.objects = { + general: { + activateGradientMiddle: true, + enableColorbrewer: false, + gradientStart: { solid: { color: "#FF0000" } }, + gradientEnd: { solid: { color: "#0000FF" } } + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + expect(readMiddlePicker()).toBeTruthy(); + done(); + }, AnimationTimeout); + }); + + it("middle picker retains a valid color when activateGradientMiddle is first enabled in colorbrewer mode", (done) => { + dataView.metadata.objects = { + general: { + activateGradientMiddle: true, + enableColorbrewer: true, + colorbrewer: "Reds", + buckets: 5 + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + expect(readMiddlePicker()).toBeTruthy(); + done(); + }, AnimationTimeout); + }); + + it("3-point palette inverts consistently when invertColorScale is on", (done) => { + const base = { + activateGradientMiddle: true, + enableColorbrewer: false, + gradientStart: { solid: { color: "#FF0000" } }, + gradientMiddle: { solid: { color: "#00FF00" } }, + gradientEnd: { solid: { color: "#0000FF" } }, + }; + + dataView.metadata.objects = { general: { ...base, invertColorScale: false } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const normalFills = getCellFills(); + + dataView.metadata.objects = { general: { ...base, invertColorScale: true } }; + visualBuilder.updateRenderTimeout(dataView, () => { + expectPaletteReversed(normalFills, getCellFills()); + done(); + }, AnimationTimeout); + }, AnimationTimeout); + }); + + it("deactivating gradient middle changes cell fills", (done) => { + const base = { + enableColorbrewer: false, + gradientStart: { solid: { color: "#FF0000" } }, + gradientMiddle: { solid: { color: "#00FF00" } }, + gradientEnd: { solid: { color: "#0000FF" } }, + }; + + dataView.metadata.objects = { general: { ...base, activateGradientMiddle: true } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const withMiddleFills = getCellFills(); + + dataView.metadata.objects = { general: { ...base, activateGradientMiddle: false } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const withoutMiddleFills = getCellFills(); + const changedCount = withMiddleFills.filter((fill, i) => !areColorsEqual(fill, withoutMiddleFills[i])).length; + expect(changedCount).toBeGreaterThan(0); + done(); + }, AnimationTimeout); + }, AnimationTimeout); + }); + + it("middle picker does NOT mutate when invertColorScale is toggled", (done) => { + const base = { + activateGradientMiddle: true, + enableColorbrewer: false, + gradientStart: { solid: { color: "#FF0000" } }, + gradientMiddle: { solid: { color: "#00FF00" } }, + gradientEnd: { solid: { color: "#0000FF" } }, + }; + + dataView.metadata.objects = { general: { ...base, invertColorScale: false } }; + visualBuilder.updateRenderTimeout(dataView, () => { + const beforeInvert = readMiddlePicker(); + + dataView.metadata.objects = { general: { ...base, invertColorScale: true } }; + visualBuilder.updateRenderTimeout(dataView, () => { + expect(areColorsEqual(readMiddlePicker(), beforeInvert)).toBeTrue(); + done(); + }, AnimationTimeout); + }, AnimationTimeout); + }); + + }); + + describe("utils:getOpacity", () => { + it("returns DefaultOpacity when no selection or highlights are active", () => { + expect(getOpacity(false, false, false, false)).toBe(DefaultOpacity); + }); + + it("returns DefaultOpacity for a selected element when selection is active", () => { + expect(getOpacity(true, false, true, false)).toBe(DefaultOpacity); + }); + + it("returns DimmedOpacity for an unselected element when selection is active", () => { + expect(getOpacity(false, false, true, false)).toBe(DimmedOpacity); + }); + + it("returns DefaultOpacity for a highlighted element when partial highlights are active", () => { + expect(getOpacity(false, true, false, true)).toBe(DefaultOpacity); + }); + + it("returns DimmedOpacity for a non-highlighted element when partial highlights are active", () => { + expect(getOpacity(false, false, false, true)).toBe(DimmedOpacity); + }); + }); + + describe("utils:heatmapUtils", () => { + describe("isDataViewValid", () => { + it("returns true when dataView has categorical categories and values", () => { + expect(isDataViewValid(defaultDataViewBuilder.getDataView())).toBeTrue(); + }); + + it("returns false when categorical is absent", () => { + expect(isDataViewValid({} as powerbi.DataView)).toBeFalse(); + }); + + it("returns false when categorical.categories is missing", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(isDataViewValid({ categorical: { values: [] } } as any)).toBeFalse(); + }); + + it("returns false when categorical.values is missing", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(isDataViewValid({ categorical: { categories: [] } } as any)).toBeFalse(); + }); + }); + + describe("textLimit", () => { + it("returns the original text when within the limit", () => { + expect(textLimit("hello", 10)).toBe("hello"); + }); + + it("truncates and appends ellipsis when over the limit", () => { + const result = textLimit("hello world", 5); + expect(result.endsWith("\u2026")).toBeTrue(); + expect(result.length).toBe(6); + }); + + it("returns empty string unchanged", () => { + expect(textLimit("", 5)).toBe(""); + }); + }); + + describe("calculateGridSizeHeight", () => { + it("clamps to ConstGridMinHeight when computed value is too small", () => { + expect(calculateGridSizeHeight(1, 500, 10, 10, 10)).toBe(ConstGridMinHeight); + }); + + it("clamps to CellMaxHeightLimit when computed value is very large", () => { + expect(calculateGridSizeHeight(100000, 0, 1, 0, 0)).toBe(CellMaxHeightLimit); + }); + + it("returns computed floor value for normal inputs", () => { + // floor((400 - 10 - 30 - 10 - 5) / (8 + 2)) = floor(34.5) = 34 + expect(calculateGridSizeHeight(400, 30, 8, 10, 10)).toBe(34); + }); + }); + + describe("calculateGridSizeWidth", () => { + it("clamps to ConstGridMinWidth when computed value is too small", () => { + expect(calculateGridSizeWidth(1, 1000, 10, 10)).toBe(ConstGridMinWidth); + }); + + it("clamps to gridSizeHeight x CellMaxWidthFactorLimit when too wide", () => { + expect(calculateGridSizeWidth(100000, 0, 1, 10)).toBe(10 * CellMaxWidthFactorLimit); + }); + + it("returns computed floor value for normal inputs", () => { + // floor((500 - 50) / 10) = 45; clamp(1, 45, 30*15) = 45 + expect(calculateGridSizeWidth(500, 50, 10, 30)).toBe(45); + }); + }); + + describe("getYAxisWidth", () => { + it("returns 0 when yAxisLabels show is false", () => { + const settings = new SettingsModel(); + settings.yAxisLabels.show.value = false; + const chartData = { categoryY: ["label"], categoryX: [] } as unknown as TableHeatMapChartData; + expect(getYAxisWidth(chartData, settings.yAxisLabels)).toBe(0); + }); + + it("returns a positive number when yAxisLabels show is true", () => { + const settings = new SettingsModel(); + const chartData = { categoryY: ["LongLabel"], categoryX: [] } as unknown as TableHeatMapChartData; + expect(getYAxisWidth(chartData, settings.yAxisLabels)).toBeGreaterThan(0); + }); + }); + + describe("getXAxisHeight", () => { + it("returns 0 when xAxisLabels show is false", () => { + const settings = new SettingsModel(); + settings.xAxisLabels.show.value = false; + const chartData = { categoryX: ["label"], categoryY: [] } as unknown as TableHeatMapChartData; + expect(getXAxisHeight(chartData, settings.xAxisLabels)).toBe(0); + }); + + it("returns measured height when xAxisLabels show is true", () => { + const settings = new SettingsModel(); + const chartData = { categoryX: ["LongLabel"], categoryY: [] } as unknown as TableHeatMapChartData; + spyOn(tms, "measureSvgTextHeight").and.returnValue(42); + expect(getXAxisHeight(chartData, settings.xAxisLabels)).toBe(42); + }); + }); + + describe("getYAxisHeight", () => { + it("returns measured height for non-empty categoryY", () => { + const settings = new SettingsModel(); + const chartData = { categoryY: ["LongLabel"], categoryX: [] } as unknown as TableHeatMapChartData; + spyOn(tms, "measureSvgTextHeight").and.returnValue(24); + expect(getYAxisHeight(chartData, settings.yAxisLabels)).toBe(24); + }); + }); + + describe("parseSettings", () => { + it("applies high contrast colors and disables colorbrewer", () => { + const palette = createColorPalette(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (palette as any).isHighContrast = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (palette as any).background = { value: "#000000" }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (palette as any).foreground = { value: "#ffff00" }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const colorHelper = new ColorHelper(palette as any); + const model = new SettingsModel(); + parseSettings(colorHelper, model); + + expect(model.general.enableColorbrewer.value).toBeFalse(); + expect(areColorsEqual(model.general.gradientStart.value.value, "#000000")).toBeTrue(); + expect(areColorsEqual(model.general.gradientEnd.value.value, "#000000")).toBeTrue(); + expect(areColorsEqual(model.labels.fill.value.value, "#ffff00")).toBeTrue(); + }); + + it("leaves settings unchanged when not in high contrast mode", () => { + const palette = createColorPalette(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (palette as any).isHighContrast = false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const colorHelper = new ColorHelper(palette as any); + const model = new SettingsModel(); + const defaultStart = model.general.gradientStart.value.value; + parseSettings(colorHelper, model); + + expect(model.general.gradientStart.value.value).toBe(defaultStart); + }); + }); + + describe("DimmedColor", () => { + it("is 'black'", () => { + expect(DimmedColor).toBe("black"); + }); + }); + }); +}); +