From a14c77028d28c9dbae6a86b159880abf3c4a0e5e Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Wed, 20 May 2026 11:42:46 +0500 Subject: [PATCH 01/19] Enhance TableHeatMap with Gradient Middle Feature --- capabilities.json | 14 + src/settings.ts | 1206 ++++++++++++----------- src/visual.ts | 51 +- stringResources/en-US/resources.resjson | 5 + 4 files changed, 690 insertions(+), 586 deletions(-) diff --git a/capabilities.json b/capabilities.json index 79e7041..0be23de 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/settings.ts b/src/settings.ts index 8fe0050..7ec09bd 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,584 +1,624 @@ -/* - * 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 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.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 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 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 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 gradientStart = new formattingSettings.ColorPicker({ + name: "gradientStart", + displayNameKey: "Visual_GradientStart", + value: { value: "#FFFFFF" }, + }); + + public activateGradientMiddle = new formattingSettings.ToggleSwitch({ + name: "activateGradientMiddle", + displayNameKey: "Visual_Activate_GradientMiddle", + value: false, + }); + + public gradientMiddle = new formattingSettings.ColorPicker({ + name: "gradientMiddle", + displayNameKey: "Visual_GradientMiddle", + value: { value: "#808080" }, + }); + + public gradientEnd = new formattingSettings.ColorPicker({ + name: "gradientEnd", + displayNameKey: "Visual_GradientEnd", + value: { value: "#000000" }, + }); + + public fillNullValuesCells = new formattingSettings.ToggleSwitch({ + name: "fillNullValuesCells", + displayNameKey: "Visual_FillNullValCell", + value: true, + }); + + public static stroke: string = "#E6E6E6"; + public textColor: string = "#AAAAAA"; + + private paletteGroup: FormattingSettingsGroup = new formattingSettings.Group({ + name: "paletteGroup", + displayNameKey: "Visual_General_Palette", + 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, this.fillNullValuesCells], + }); + + private gradientScaleGroup: FormattingSettingsGroup = new formattingSettings.Group({ + name: "gradientScaleGroup", + displayNameKey: "Visual_General_GradientScale", + collapsible: false, + slices: [this.buckets], + }); + + 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: 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; + } + } } \ No newline at end of file diff --git a/src/visual.ts b/src/visual.ts index 2263082..90c4705 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -518,14 +518,33 @@ export class TableHeatMap implements IVisual { const minDataValue: number = d3Min(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); const maxDataValue: number = d3Max(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); - const colors: string[] = this.initColors(settingsModel); - + if (settingsModel.general.activateGradientMiddle.value && + settingsModel.general.gradientMiddle.value.value === "#808080") { + const cbEnable: boolean = settingsModel.general.enableColorbrewer.value; + const cbScale: string = settingsModel.general.colorbrewer.value.toString(); + const numBuckets: number = settingsModel.CurrentBucketCount; + let autoStart: string; + let autoEnd: string; + if (cbEnable) { + const cbPalette: IColorArray = colorbrewer[cbScale] || colorbrewer.Reds; + const cbColors: string[] = cbPalette[numBuckets] || colorbrewer.Reds[numBuckets]; + autoStart = cbColors[0]; + autoEnd = cbColors[cbColors.length - 1]; + } else { + autoStart = settingsModel.general.gradientStart.value.value; + autoEnd = settingsModel.general.gradientEnd.value.value; + } + const midScale: LinearColorScale = createLinearColorScale([0, 1], [autoStart, autoEnd], true); + settingsModel.general.gradientMiddle.value.value = midScale(0.5); + } + const colors: string[] = this.initColors(settingsModel); const colorScale: Quantile = d3ScaleQuantile() .domain([minDataValue, maxDataValue]) .range(colors); settingsModel.general.gradientStart.value.value = colors[0]; + settingsModel.general.gradientMiddle.value.value = colors[Math.floor((colors.length - 1) / 2)]; settingsModel.general.gradientEnd.value.value = colors[colors.length - 1]; const renderOptions: IRenderOptions = { @@ -548,10 +567,36 @@ 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; let colors: Array; - if (colorbrewerEnable) { + + if (activateGradientMiddle) { + // Determine start and end from colorbrewer palette or user settings + let startColor: string; + let endColor: string; + if (colorbrewerEnable) { + const currentColorbrewer: IColorArray = colorbrewer[colorbrewerScale] || colorbrewer.Reds; + const palette: string[] = currentColorbrewer[numBuckets] || colorbrewer.Reds[numBuckets]; + startColor = palette[0]; + endColor = palette[palette.length - 1]; + } else { + startColor = settingsModel.general.gradientStart.value.value; + endColor = settingsModel.general.gradientEnd.value.value; + } + const middleColor: string = settingsModel.general.gradientMiddle.value.value; + const midIndex: number = Math.floor((numBuckets - 1) / 2); + const colorScale: LinearColorScale = createLinearColorScale( + [0, midIndex, numBuckets - 1], + [startColor, middleColor, endColor], + true + ); + colors = []; + for (let bucketIndex: number = 0; bucketIndex < numBuckets; bucketIndex++) { + colors.push(colorScale(bucketIndex)); + } + } else if (colorbrewerEnable) { if (colorbrewerScale) { const currentColorbrewer: IColorArray = colorbrewer[colorbrewerScale]; colors = (currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]); diff --git a/stringResources/en-US/resources.resjson b/stringResources/en-US/resources.resjson index b762e60..61bfcf9 100644 --- a/stringResources/en-US/resources.resjson +++ b/stringResources/en-US/resources.resjson @@ -16,6 +16,11 @@ "Visual_Font": "Font", "Visual_GradientStart": "Gradient start", "Visual_GradientEnd": "Gradient end", + "Visual_Activate_GradientMiddle": "Add gradient middle", + "Visual_GradientMiddle": "Gradient middle", + "Visual_General_Gradient": "Gradient Colors", + "Visual_General_Palette": "Color Palette", + "Visual_General_GradientScale": "Gradient scale", "Visual_ForceDisplay": "Force display", "Visual_XAxis": "X axis data labels", "Visual_YAxis": "Y axis data labels", From ba14d84d3bc2a3696d2ececd193b120717f8c070 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Fri, 22 May 2026 16:47:33 +0500 Subject: [PATCH 02/19] Update gradient middle color handling in TableHeatMap --- src/settings.ts | 2 +- src/visual.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 7ec09bd..fd4cf3c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -425,7 +425,7 @@ export class GeneralSettings extends FormattingSettingsCompositeCard { public gradientMiddle = new formattingSettings.ColorPicker({ name: "gradientMiddle", displayNameKey: "Visual_GradientMiddle", - value: { value: "#808080" }, + value: { value: "" }, }); public gradientEnd = new formattingSettings.ColorPicker({ diff --git a/src/visual.ts b/src/visual.ts index 90c4705..0976644 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -573,7 +573,6 @@ export class TableHeatMap implements IVisual { let colors: Array; if (activateGradientMiddle) { - // Determine start and end from colorbrewer palette or user settings let startColor: string; let endColor: string; if (colorbrewerEnable) { @@ -586,10 +585,17 @@ export class TableHeatMap implements IVisual { endColor = settingsModel.general.gradientEnd.value.value; } const middleColor: string = settingsModel.general.gradientMiddle.value.value; - const midIndex: number = Math.floor((numBuckets - 1) / 2); + const range: string[] = [startColor, endColor]; + const domain: number[] = [0, numBuckets - 1]; + + if (middleColor !== '') { + range.splice(1, 0, middleColor); + domain.splice(1, 0, Math.floor((numBuckets - 1) / 2)); + } + const colorScale: LinearColorScale = createLinearColorScale( - [0, midIndex, numBuckets - 1], - [startColor, middleColor, endColor], + domain, + range, true ); colors = []; @@ -599,7 +605,7 @@ export class TableHeatMap implements IVisual { } else if (colorbrewerEnable) { if (colorbrewerScale) { const currentColorbrewer: IColorArray = colorbrewer[colorbrewerScale]; - colors = (currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]); + colors = currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]; } else { colors = colorbrewer.Reds[numBuckets]; // default color scheme From c5d164d70b973301f4e000313087c4b4ead435bf Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Sat, 30 May 2026 13:40:58 +0500 Subject: [PATCH 03/19] Revert: remove diverging gradient middle feature --- capabilities.json | 14 - src/settings.ts | 1206 +++++++++++------------ src/visual.ts | 59 +- stringResources/en-US/resources.resjson | 5 - 4 files changed, 587 insertions(+), 697 deletions(-) diff --git a/capabilities.json b/capabilities.json index 0be23de..79e7041 100644 --- a/capabilities.json +++ b/capabilities.json @@ -227,20 +227,6 @@ } } }, - "activateGradientMiddle": { - "type": { - "bool": true - } - }, - "gradientMiddle": { - "type": { - "fill": { - "solid": { - "color": true - } - } - } - }, "gradientEnd": { "type": { "fill": { diff --git a/src/settings.ts b/src/settings.ts index fd4cf3c..8fe0050 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,624 +1,584 @@ -/* - * 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 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 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 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 gradientStart = new formattingSettings.ColorPicker({ - name: "gradientStart", - displayNameKey: "Visual_GradientStart", - value: { value: "#FFFFFF" }, - }); - - public activateGradientMiddle = new formattingSettings.ToggleSwitch({ - name: "activateGradientMiddle", - displayNameKey: "Visual_Activate_GradientMiddle", - value: false, - }); - - public gradientMiddle = new formattingSettings.ColorPicker({ - name: "gradientMiddle", - displayNameKey: "Visual_GradientMiddle", - value: { value: "" }, - }); - - public gradientEnd = new formattingSettings.ColorPicker({ - name: "gradientEnd", - displayNameKey: "Visual_GradientEnd", - value: { value: "#000000" }, - }); - - public fillNullValuesCells = new formattingSettings.ToggleSwitch({ - name: "fillNullValuesCells", - displayNameKey: "Visual_FillNullValCell", - value: true, - }); - - public static stroke: string = "#E6E6E6"; - public textColor: string = "#AAAAAA"; - - private paletteGroup: FormattingSettingsGroup = new formattingSettings.Group({ - name: "paletteGroup", - displayNameKey: "Visual_General_Palette", - 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, this.fillNullValuesCells], - }); - - private gradientScaleGroup: FormattingSettingsGroup = new formattingSettings.Group({ - name: "gradientScaleGroup", - displayNameKey: "Visual_General_GradientScale", - collapsible: false, - slices: [this.buckets], - }); - - 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: 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 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 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.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; + } + } } \ No newline at end of file diff --git a/src/visual.ts b/src/visual.ts index 0976644..2263082 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -518,33 +518,14 @@ export class TableHeatMap implements IVisual { const minDataValue: number = d3Min(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); const maxDataValue: number = d3Max(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); - if (settingsModel.general.activateGradientMiddle.value && - settingsModel.general.gradientMiddle.value.value === "#808080") { - const cbEnable: boolean = settingsModel.general.enableColorbrewer.value; - const cbScale: string = settingsModel.general.colorbrewer.value.toString(); - const numBuckets: number = settingsModel.CurrentBucketCount; - let autoStart: string; - let autoEnd: string; - if (cbEnable) { - const cbPalette: IColorArray = colorbrewer[cbScale] || colorbrewer.Reds; - const cbColors: string[] = cbPalette[numBuckets] || colorbrewer.Reds[numBuckets]; - autoStart = cbColors[0]; - autoEnd = cbColors[cbColors.length - 1]; - } else { - autoStart = settingsModel.general.gradientStart.value.value; - autoEnd = settingsModel.general.gradientEnd.value.value; - } - const midScale: LinearColorScale = createLinearColorScale([0, 1], [autoStart, autoEnd], true); - settingsModel.general.gradientMiddle.value.value = midScale(0.5); - } - const colors: string[] = this.initColors(settingsModel); + + const colorScale: Quantile = d3ScaleQuantile() .domain([minDataValue, maxDataValue]) .range(colors); settingsModel.general.gradientStart.value.value = colors[0]; - settingsModel.general.gradientMiddle.value.value = colors[Math.floor((colors.length - 1) / 2)]; settingsModel.general.gradientEnd.value.value = colors[colors.length - 1]; const renderOptions: IRenderOptions = { @@ -567,45 +548,13 @@ 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; let colors: Array; - - if (activateGradientMiddle) { - let startColor: string; - let endColor: string; - if (colorbrewerEnable) { - const currentColorbrewer: IColorArray = colorbrewer[colorbrewerScale] || colorbrewer.Reds; - const palette: string[] = currentColorbrewer[numBuckets] || colorbrewer.Reds[numBuckets]; - startColor = palette[0]; - endColor = palette[palette.length - 1]; - } else { - startColor = settingsModel.general.gradientStart.value.value; - endColor = settingsModel.general.gradientEnd.value.value; - } - const middleColor: string = settingsModel.general.gradientMiddle.value.value; - const range: string[] = [startColor, endColor]; - const domain: number[] = [0, numBuckets - 1]; - - if (middleColor !== '') { - range.splice(1, 0, middleColor); - domain.splice(1, 0, Math.floor((numBuckets - 1) / 2)); - } - - const colorScale: LinearColorScale = createLinearColorScale( - domain, - range, - true - ); - colors = []; - for (let bucketIndex: number = 0; bucketIndex < numBuckets; bucketIndex++) { - colors.push(colorScale(bucketIndex)); - } - } else if (colorbrewerEnable) { + if (colorbrewerEnable) { if (colorbrewerScale) { const currentColorbrewer: IColorArray = colorbrewer[colorbrewerScale]; - colors = currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]; + colors = (currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]); } else { colors = colorbrewer.Reds[numBuckets]; // default color scheme diff --git a/stringResources/en-US/resources.resjson b/stringResources/en-US/resources.resjson index 61bfcf9..b762e60 100644 --- a/stringResources/en-US/resources.resjson +++ b/stringResources/en-US/resources.resjson @@ -16,11 +16,6 @@ "Visual_Font": "Font", "Visual_GradientStart": "Gradient start", "Visual_GradientEnd": "Gradient end", - "Visual_Activate_GradientMiddle": "Add gradient middle", - "Visual_GradientMiddle": "Gradient middle", - "Visual_General_Gradient": "Gradient Colors", - "Visual_General_Palette": "Color Palette", - "Visual_General_GradientScale": "Gradient scale", "Visual_ForceDisplay": "Force display", "Visual_XAxis": "X axis data labels", "Visual_YAxis": "Y axis data labels", From fa462bc96b1c8f277f6c793ee2f69c14df10d84f Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Mon, 1 Jun 2026 10:59:06 +0500 Subject: [PATCH 04/19] feat: add gradient middle color feature with toggle and picker in Format pane --- CHANGELOG.md | 2 + capabilities.json | 14 + src/heatmapUtils.ts | 111 ++ src/settings.ts | 52 +- src/visual.ts | 180 +- stringResources/en-US/resources.resjson | 4 + test/visualTest.ts | 2004 +++++++++++++---------- 7 files changed, 1395 insertions(+), 972 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e5553..191ebe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ ### 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 a user-defined or auto-computed midpoint colour. ### 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..9a97eeb 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 { TableHeatMapChartData } from "./dataInterfaces"; +import { BaseLabelCardSettings, GeneralSettings, SettingsModel, YAxisLabelsSettings } from "./settings"; + export const DimmedOpacity: number = 0.4; export const DefaultOpacity: number = 1.0; export const DimmedColor: string = "black"; @@ -38,4 +50,103 @@ export function getOpacity( } return DefaultOpacity; +} + +export const YAxisAdditionalMargin: number = 5; +export const AdditionalSpaceForColorbrewerCells: 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 + AdditionalSpaceForColorbrewerCells) + ); + + return Math.max(ConstGridMinHeight, Math.min(gridSizeHeight, CellMaxHeightLimit)); +} + +export function calculateGridSizeWidth( + viewportWidth: number, + yAxisWidth: number, + categoryXLength: number, + gridSizeHeight: number +): number { + const gridSizeWidth: number = Math.floor((viewportWidth - yAxisWidth) / categoryXLength); + + return Math.max(ConstGridMinWidth, Math.min(gridSizeWidth, gridSizeHeight * CellMaxWidthFactorLimit)); +} + +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.gradientStart.value.value = backgroundColor; + settingsModel.general.gradientEnd.value.value = backgroundColor; + GeneralSettings.stroke = foregroundColor; + settingsModel.general.textColor = foregroundColor; + } + + return settingsModel; } \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts index a78b4b6..567807f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -26,6 +26,8 @@ import { formattingSettings } from "powerbi-visuals-utils-formattingmodel"; import FormattingSettingsSimpleCard = formattingSettings.SimpleCard; +import FormattingSettingsCompositeCard = formattingSettings.CompositeCard; +import FormattingSettingsGroup = formattingSettings.Group; import FormattingSettingsSlice = formattingSettings.Slice; import FormattingSettingsModel = formattingSettings.Model; @@ -370,7 +372,7 @@ export const colorbrewer: IColorBrewer = { } }; -export class GeneralSettings extends FormattingSettingsSimpleCard { +export class GeneralSettings extends FormattingSettingsCompositeCard { public name: string = "general"; public displayNameKey: string = "Visual_General"; @@ -404,6 +406,18 @@ export class GeneralSettings extends FormattingSettingsSimpleCard { value: { value: "#000000" }, }); + public activateGradientMiddle = new formattingSettings.ToggleSwitch({ + name: "activateGradientMiddle", + displayNameKey: "Visual_Activate_GradientMiddle", + value: false, + }); + + public gradientMiddle = new formattingSettings.ColorPicker({ + name: "gradientMiddle", + displayNameKey: "Visual_GradientMiddle", + value: { value: "" }, + }); + public invertColorScale = new formattingSettings.ToggleSwitch({ name: "invertColorScale", displayNameKey: "Visual_InvertColorScale", @@ -436,15 +450,33 @@ export class GeneralSettings extends FormattingSettingsSimpleCard { 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 - ]; + 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.invertColorScale, this.buckets, this.fillNullValuesCells], + }); + + public groups: FormattingSettingsGroup[] = [this.paletteGroup, this.gradientGroup, this.gradientScaleGroup]; + + public onPreProcess(): void { + this.gradientMiddle.visible = this.activateGradientMiddle.value; + } } export class BaseLabelCardSettings extends FormattingSettingsSimpleCard { diff --git a/src/visual.ts b/src/visual.ts index 4fd99ac..e13065b 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; @@ -79,10 +77,21 @@ import { BaseLabelCardSettings, GeneralSettings, SettingsModel, - YAxisLabelsSettings, colorbrewer } from "./settings"; +import { + calculateGridSizeHeight, + calculateGridSizeWidth, + CellMaxHeightLimit, + getXAxisHeight, + getYAxisHeight, + getYAxisWidth, + isDataViewValid, + parseSettings, + 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 = "*"; @@ -156,8 +163,6 @@ export class TableHeatMap implements IVisual { 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 +171,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 +316,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 +327,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 +337,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 +377,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 +408,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,19 +434,43 @@ 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); + // Auto-compute the gradient middle colour on the first activation (sentinel = ""). + if (settingsModel.general.activateGradientMiddle.value && + settingsModel.general.gradientMiddle.value.value === "") { + console.log({ + activateGradientMiddle: settingsModel.general.activateGradientMiddle.value, + gradientMiddle: settingsModel.general.gradientMiddle.value.value, + }) + const cbEnable: boolean = settingsModel.general.enableColorbrewer.value; + const cbScale: string = settingsModel.general.colorbrewer.value.toString(); + const numBuckets: number = settingsModel.CurrentBucketCount; + let autoStart: string, autoEnd: string; + if (cbEnable) { + const cbPalette: IColorArray = colorbrewer[cbScale] || colorbrewer.Reds; + const cbColors: string[] = cbPalette[numBuckets] || colorbrewer.Reds[numBuckets]; + autoStart = cbColors[0]; + autoEnd = cbColors[cbColors.length - 1]; + } else { + autoStart = settingsModel.general.gradientStart.value.value; + autoEnd = settingsModel.general.gradientEnd.value.value; + } + const midScale: LinearColorScale = createLinearColorScale([0, 1], [autoStart, autoEnd], true); + settingsModel.general.gradientMiddle.value.value = midScale(0.5); + } + // Base palette as defined by the active source (colorbrewer or custom gradient), // without invert applied. Used both for rendering and for syncing gradient pickers. const baseColors: string[] = this.initColors(settingsModel); @@ -532,6 +485,13 @@ export class TableHeatMap implements IVisual { settingsModel.general.gradientEnd.value.value = baseColors[baseColors.length - 1]; } + // Keep the middle picker in sync with the rendered palette so the Format pane + // always reflects what is on screen (uses pre-invert baseColors, same as start/end). + if (settingsModel.general.activateGradientMiddle.value) { + settingsModel.general.gradientMiddle.value.value = + baseColors[Math.floor((baseColors.length - 1) / 2)]; + } + // Invert is a render-only transformation applied as the final step so that toggling // it never mutates user-visible settings (gradient pickers stay stable). const colors: string[] = settingsModel.general.invertColorScale.value @@ -562,8 +522,36 @@ 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) { + let startColor: string, endColor: string; + + if (colorbrewerEnable) { + const currentColorbrewer: IColorArray = colorbrewerScale ? colorbrewer[colorbrewerScale] : undefined; + const palette: string[] = currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]; + startColor = palette[0]; + endColor = palette[palette.length - 1]; + } else { + startColor = settingsModel.general.gradientStart.value.value; + endColor = settingsModel.general.gradientEnd.value.value; + } + + const middleColor: string = settingsModel.general.gradientMiddle.value.value; + const midIndex: number = Math.floor((numBuckets - 1) / 2); + const domain: number[] = [0, midIndex, 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]); @@ -685,7 +673,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) @@ -871,14 +859,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..8812b30 100644 --- a/stringResources/en-US/resources.resjson +++ b/stringResources/en-US/resources.resjson @@ -1,4 +1,5 @@ { + "Visual_Activate_GradientMiddle": "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..ae1d259 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -1,862 +1,1142 @@ -/* -* 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).toBeTruthy(); + 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).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 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).toBeTruthy(); + 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).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 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).toBeTruthy(); + 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).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")).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("auto-computes middle color from sentinel on first activation 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("auto-computes middle color from colorbrewer palette midpoint on first activation", (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); + }); + + it("middle picker is an interpolated blend between the colorbrewer palette endpoints", (done) => { + dataView.metadata.objects = { + general: { + activateGradientMiddle: true, + enableColorbrewer: true, + colorbrewer: "Reds", + buckets: 5 + } + }; + + visualBuilder.updateRenderTimeout(dataView, () => { + const middle = readMiddlePicker(); + expect(middle).toBeTruthy(); + // The auto-computed middle is a linear blend — it must differ from both palette endpoints. + const r5 = colorbrewer.Reds[5]; + expect(areColorsEqual(middle, r5[0])).toBeFalse(); + expect(areColorsEqual(middle, r5[r5.length - 1])).toBeFalse(); + done(); + }, 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 a number when xAxisLabels show is true", () => { + const settings = new SettingsModel(); + const chartData = { categoryX: ["LongLabel"], categoryY: [] } as unknown as TableHeatMapChartData; + // measureSvgTextHeight may return 0 in headless environments; assert non-negative. + expect(getXAxisHeight(chartData, settings.xAxisLabels)).toBeGreaterThanOrEqual(0); + }); + }); + + describe("getYAxisHeight", () => { + it("returns a number for non-empty categoryY", () => { + const settings = new SettingsModel(); + const chartData = { categoryY: ["LongLabel"], categoryX: [] } as unknown as TableHeatMapChartData; + // measureSvgTextHeight may return 0 in headless environments; assert non-negative. + expect(getYAxisHeight(chartData, settings.yAxisLabels)).toBeGreaterThanOrEqual(0); + }); + }); + + 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"); + }); + }); + }); +}); + From f7cca159ee5b6a87f99e64af8fdad39f684846ec Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Mon, 1 Jun 2026 11:21:49 +0500 Subject: [PATCH 05/19] feat: add minimum width check for grid size calculation and remove debug logs for gradient middle color --- src/heatmapUtils.ts | 3 +++ src/visual.ts | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/heatmapUtils.ts b/src/heatmapUtils.ts index 9a97eeb..0ac5f7a 100644 --- a/src/heatmapUtils.ts +++ b/src/heatmapUtils.ts @@ -125,6 +125,9 @@ export function calculateGridSizeWidth( 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)); diff --git a/src/visual.ts b/src/visual.ts index e13065b..5a3e04d 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -450,10 +450,6 @@ export class TableHeatMap implements IVisual { // Auto-compute the gradient middle colour on the first activation (sentinel = ""). if (settingsModel.general.activateGradientMiddle.value && settingsModel.general.gradientMiddle.value.value === "") { - console.log({ - activateGradientMiddle: settingsModel.general.activateGradientMiddle.value, - gradientMiddle: settingsModel.general.gradientMiddle.value.value, - }) const cbEnable: boolean = settingsModel.general.enableColorbrewer.value; const cbScale: string = settingsModel.general.colorbrewer.value.toString(); const numBuckets: number = settingsModel.CurrentBucketCount; From 91095a205507530e717e9a52608bfdf594db38de Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Mon, 1 Jun 2026 11:34:42 +0500 Subject: [PATCH 06/19] feat: centralize color resolution logic for gradient calculations in heatmap --- src/heatmapUtils.ts | 24 ++++++++++++++++++++++-- src/visual.ts | 44 ++++++++++++++++++++------------------------ 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/heatmapUtils.ts b/src/heatmapUtils.ts index 0ac5f7a..00f9617 100644 --- a/src/heatmapUtils.ts +++ b/src/heatmapUtils.ts @@ -32,8 +32,8 @@ import { ColorHelper } from "powerbi-visuals-utils-colorutils"; import maxBy from "lodash.maxby"; -import { TableHeatMapChartData } from "./dataInterfaces"; -import { BaseLabelCardSettings, GeneralSettings, SettingsModel, YAxisLabelsSettings } from "./settings"; +import { IColorArray, TableHeatMapChartData } from "./dataInterfaces"; +import { BaseLabelCardSettings, colorbrewer, GeneralSettings, SettingsModel, YAxisLabelsSettings } from "./settings"; export const DimmedOpacity: number = 0.4; export const DefaultOpacity: number = 1.0; @@ -133,6 +133,26 @@ export function calculateGridSizeWidth( 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). Centralises the fallback logic so it cannot drift between + * `createRenderOptions` and `initColors`. + */ +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[] = palette[numBuckets] || colorbrewer.Reds[numBuckets]; + 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"); diff --git a/src/visual.ts b/src/visual.ts index 5a3e04d..f29b64d 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -89,6 +89,7 @@ import { getYAxisWidth, isDataViewValid, parseSettings, + resolveStartEndColors, textLimit } from "./heatmapUtils"; @@ -450,21 +451,20 @@ export class TableHeatMap implements IVisual { // Auto-compute the gradient middle colour on the first activation (sentinel = ""). if (settingsModel.general.activateGradientMiddle.value && settingsModel.general.gradientMiddle.value.value === "") { - const cbEnable: boolean = settingsModel.general.enableColorbrewer.value; - const cbScale: string = settingsModel.general.colorbrewer.value.toString(); const numBuckets: number = settingsModel.CurrentBucketCount; - let autoStart: string, autoEnd: string; - if (cbEnable) { - const cbPalette: IColorArray = colorbrewer[cbScale] || colorbrewer.Reds; - const cbColors: string[] = cbPalette[numBuckets] || colorbrewer.Reds[numBuckets]; - autoStart = cbColors[0]; - autoEnd = cbColors[cbColors.length - 1]; - } else { - autoStart = settingsModel.general.gradientStart.value.value; - autoEnd = settingsModel.general.gradientEnd.value.value; - } + const { startColor: autoStart, endColor: autoEnd } = resolveStartEndColors( + settingsModel.general.enableColorbrewer.value, + settingsModel.general.colorbrewer.value.toString(), + numBuckets, + settingsModel.general.gradientStart.value.value, + settingsModel.general.gradientEnd.value.value + ); + // Use the same midpoint fraction as the 3-stop scale in initColors so the + // auto-derived colour corresponds to the bucket that will be pinned there. + const midIndex: number = Math.floor((numBuckets - 1) / 2); + const midPos: number = numBuckets > 1 ? midIndex / (numBuckets - 1) : 0; const midScale: LinearColorScale = createLinearColorScale([0, 1], [autoStart, autoEnd], true); - settingsModel.general.gradientMiddle.value.value = midScale(0.5); + settingsModel.general.gradientMiddle.value.value = midScale(midPos); } // Base palette as defined by the active source (colorbrewer or custom gradient), @@ -522,17 +522,13 @@ export class TableHeatMap implements IVisual { const numBuckets: number = settingsModel.CurrentBucketCount; if (activateGradientMiddle) { - let startColor: string, endColor: string; - - if (colorbrewerEnable) { - const currentColorbrewer: IColorArray = colorbrewerScale ? colorbrewer[colorbrewerScale] : undefined; - const palette: string[] = currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]; - startColor = palette[0]; - endColor = palette[palette.length - 1]; - } else { - startColor = settingsModel.general.gradientStart.value.value; - endColor = settingsModel.general.gradientEnd.value.value; - } + 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 midIndex: number = Math.floor((numBuckets - 1) / 2); From 666cc4a8c640b4af435e34e6a10eb2aaa2ab8dc2 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Mon, 1 Jun 2026 13:07:32 +0500 Subject: [PATCH 07/19] feat: enhance gradient color handling and improve test descriptions for clarity --- src/heatmapUtils.ts | 7 ++++++- src/visual.ts | 9 ++++++--- test/visualTest.ts | 12 ++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/heatmapUtils.ts b/src/heatmapUtils.ts index 00f9617..4d4e4da 100644 --- a/src/heatmapUtils.ts +++ b/src/heatmapUtils.ts @@ -147,7 +147,12 @@ export function resolveStartEndColors( ): { startColor: string; endColor: string } { if (colorbrewerEnable) { const palette: IColorArray = colorbrewer[colorbrewerScale] || colorbrewer.Reds; - const colors: string[] = palette[numBuckets] || colorbrewer.Reds[numBuckets]; + 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 }; diff --git a/src/visual.ts b/src/visual.ts index f29b64d..293f660 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -481,9 +481,12 @@ export class TableHeatMap implements IVisual { settingsModel.general.gradientEnd.value.value = baseColors[baseColors.length - 1]; } - // Keep the middle picker in sync with the rendered palette so the Format pane - // always reflects what is on screen (uses pre-invert baseColors, same as start/end). - if (settingsModel.general.activateGradientMiddle.value) { + // Keep the middle picker in sync with the rendered colorbrewer palette so the + // Format pane reflects what is on screen. Skipped in custom-gradient mode because + // the picker is the source of truth there — overwriting it would cause format-pane + // churn and corrupt the user's chosen middle colour. + if (settingsModel.general.activateGradientMiddle.value && + settingsModel.general.enableColorbrewer.value) { settingsModel.general.gradientMiddle.value.value = baseColors[Math.floor((baseColors.length - 1) / 2)]; } diff --git a/test/visualTest.ts b/test/visualTest.ts index ae1d259..919fe57 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -1081,20 +1081,20 @@ describe("TableHeatmap", () => { expect(getXAxisHeight(chartData, settings.xAxisLabels)).toBe(0); }); - it("returns a number when xAxisLabels show is true", () => { + it("returns measured height when xAxisLabels show is true", () => { const settings = new SettingsModel(); const chartData = { categoryX: ["LongLabel"], categoryY: [] } as unknown as TableHeatMapChartData; - // measureSvgTextHeight may return 0 in headless environments; assert non-negative. - expect(getXAxisHeight(chartData, settings.xAxisLabels)).toBeGreaterThanOrEqual(0); + spyOn(tms, "measureSvgTextHeight").and.returnValue(42); + expect(getXAxisHeight(chartData, settings.xAxisLabels)).toBe(42); }); }); describe("getYAxisHeight", () => { - it("returns a number for non-empty categoryY", () => { + it("returns measured height for non-empty categoryY", () => { const settings = new SettingsModel(); const chartData = { categoryY: ["LongLabel"], categoryX: [] } as unknown as TableHeatMapChartData; - // measureSvgTextHeight may return 0 in headless environments; assert non-negative. - expect(getYAxisHeight(chartData, settings.yAxisLabels)).toBeGreaterThanOrEqual(0); + spyOn(tms, "measureSvgTextHeight").and.returnValue(24); + expect(getYAxisHeight(chartData, settings.yAxisLabels)).toBe(24); }); }); From cf16b5911823e5fabd31c050775f79d64a4f3e0b Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Tue, 2 Jun 2026 22:27:21 +0500 Subject: [PATCH 08/19] feat: improve colorbrewer palette handling and update test description for middle color computation --- src/visual.ts | 11 +++++++---- test/visualTest.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/visual.ts b/src/visual.ts index 293f660..830c1bd 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -548,10 +548,13 @@ export class TableHeatMap implements IVisual { } 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; diff --git a/test/visualTest.ts b/test/visualTest.ts index 919fe57..b4cd05e 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -867,7 +867,7 @@ describe("TableHeatmap", () => { }, AnimationTimeout); }); - it("auto-computes middle color from colorbrewer palette midpoint on first activation", (done) => { + it("auto-computes middle color as a blend between colorbrewer endpoints on first activation", (done) => { dataView.metadata.objects = { general: { activateGradientMiddle: true, From 2c7081a3e221c99bd8300a1092e508f8e2022f6d Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 4 Jun 2026 21:49:10 +0500 Subject: [PATCH 09/19] fix: update key for gradient middle activation in resource file --- src/settings.ts | 1260 +++++++++++------------ stringResources/en-US/resources.resjson | 2 +- 2 files changed, 631 insertions(+), 631 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 567807f..5ddbcf6 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,631 +1,631 @@ -/* - * 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 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 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_Activate_GradientMiddle", - value: false, - }); - - public gradientMiddle = new formattingSettings.ColorPicker({ - name: "gradientMiddle", - displayNameKey: "Visual_GradientMiddle", - value: { value: "" }, - }); - - 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"; - - 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.invertColorScale, this.buckets, this.fillNullValuesCells], - }); - - 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: 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 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 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: "" }, + }); + + 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"; + + 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.invertColorScale, this.buckets, this.fillNullValuesCells], + }); + + 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: 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; + } + } } \ No newline at end of file diff --git a/stringResources/en-US/resources.resjson b/stringResources/en-US/resources.resjson index 8812b30..1358c78 100644 --- a/stringResources/en-US/resources.resjson +++ b/stringResources/en-US/resources.resjson @@ -1,5 +1,5 @@ { - "Visual_Activate_GradientMiddle": "Add gradient middle", + "Visual_ActivateGradientMiddle": "Add gradient middle", "Visual_Category": "Category", "Visual_DataLabels": "Data labels", "Visual_Description_DisplayAllLabelsAnyway": "Display all labels anyway", From ffc6a960950273819b2f6b72b9d6910170abf12a Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 4 Jun 2026 21:51:46 +0500 Subject: [PATCH 10/19] feat: add minimum limit for bucket count with gradient middle and update gradient middle color handling --- src/settings.ts | 9 +++++++-- src/visual.ts | 42 ++++++++++++------------------------------ 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 5ddbcf6..5ffc598 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -379,6 +379,7 @@ export class GeneralSettings extends FormattingSettingsCompositeCard { 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; @@ -415,7 +416,7 @@ export class GeneralSettings extends FormattingSettingsCompositeCard { public gradientMiddle = new formattingSettings.ColorPicker({ name: "gradientMiddle", displayNameKey: "Visual_GradientMiddle", - value: { value: "" }, + value: { value: "#767676" }, }); public invertColorScale = new formattingSettings.ToggleSwitch({ @@ -619,13 +620,17 @@ export class SettingsModel extends FormattingSettingsModel { 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, GeneralSettings.BucketCountMinLimit) + Math.max(currentValue, minLimit) ); this.CurrentBucketCount = this.general.buckets.value = clampedValue; + this.general.buckets.options.minValue.value = minLimit; } } } \ No newline at end of file diff --git a/src/visual.ts b/src/visual.ts index 830c1bd..e9c06be 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -448,25 +448,6 @@ export class TableHeatMap implements IVisual { const minDataValue: number = d3Min(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); const maxDataValue: number = d3Max(chartData.dataPoints, (d: TableHeatMapDataPoint) => d.value as number); - // Auto-compute the gradient middle colour on the first activation (sentinel = ""). - if (settingsModel.general.activateGradientMiddle.value && - settingsModel.general.gradientMiddle.value.value === "") { - const numBuckets: number = settingsModel.CurrentBucketCount; - const { startColor: autoStart, endColor: autoEnd } = resolveStartEndColors( - settingsModel.general.enableColorbrewer.value, - settingsModel.general.colorbrewer.value.toString(), - numBuckets, - settingsModel.general.gradientStart.value.value, - settingsModel.general.gradientEnd.value.value - ); - // Use the same midpoint fraction as the 3-stop scale in initColors so the - // auto-derived colour corresponds to the bucket that will be pinned there. - const midIndex: number = Math.floor((numBuckets - 1) / 2); - const midPos: number = numBuckets > 1 ? midIndex / (numBuckets - 1) : 0; - const midScale: LinearColorScale = createLinearColorScale([0, 1], [autoStart, autoEnd], true); - settingsModel.general.gradientMiddle.value.value = midScale(midPos); - } - // Base palette as defined by the active source (colorbrewer or custom gradient), // without invert applied. Used both for rendering and for syncing gradient pickers. const baseColors: string[] = this.initColors(settingsModel); @@ -481,16 +462,6 @@ export class TableHeatMap implements IVisual { settingsModel.general.gradientEnd.value.value = baseColors[baseColors.length - 1]; } - // Keep the middle picker in sync with the rendered colorbrewer palette so the - // Format pane reflects what is on screen. Skipped in custom-gradient mode because - // the picker is the source of truth there — overwriting it would cause format-pane - // churn and corrupt the user's chosen middle colour. - if (settingsModel.general.activateGradientMiddle.value && - settingsModel.general.enableColorbrewer.value) { - settingsModel.general.gradientMiddle.value.value = - baseColors[Math.floor((baseColors.length - 1) / 2)]; - } - // Invert is a render-only transformation applied as the final step so that toggling // it never mutates user-visible settings (gradient pickers stay stable). const colors: string[] = settingsModel.general.invertColorScale.value @@ -533,8 +504,19 @@ export class TableHeatMap implements IVisual { settingsModel.general.gradientEnd.value.value ); - const middleColor: string = settingsModel.general.gradientMiddle.value.value; + if (numBuckets < 3) { + const fallbackScale: LinearColorScale = createLinearColorScale([0, Math.max(numBuckets - 1, 1)], [startColor, endColor], true); + const colors: string[] = []; + for (let i: number = 0; i < numBuckets; i++) { + colors.push(fallbackScale(i)); + } + return colors; + } + + const storedMiddle: string = settingsModel.general.gradientMiddle.value.value; const midIndex: number = Math.floor((numBuckets - 1) / 2); + const midPos: number = midIndex / (numBuckets - 1); + const middleColor: string = storedMiddle || createLinearColorScale([0, 1], [startColor, endColor], true)(midPos); const domain: number[] = [0, midIndex, numBuckets - 1]; const range: string[] = [startColor, middleColor, endColor]; const colorScale: LinearColorScale = createLinearColorScale(domain, range, true); From 5f58aa5912f0f91028949fc04eea0d25ee87ec8c Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 4 Jun 2026 21:51:57 +0500 Subject: [PATCH 11/19] refactor: update test descriptions for middle color handling in gradient modes --- test/visualTest.ts | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/test/visualTest.ts b/test/visualTest.ts index b4cd05e..d17fd98 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -851,7 +851,7 @@ describe("TableHeatmap", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const readMiddlePicker = (): string => (visualBuilder as any).visual.settingsModel.general.gradientMiddle.value.value; - it("auto-computes middle color from sentinel on first activation in custom gradient mode", (done) => { + it("middle picker retains a valid color when activateGradientMiddle is first enabled in custom gradient mode", (done) => { dataView.metadata.objects = { general: { activateGradientMiddle: true, @@ -867,7 +867,7 @@ describe("TableHeatmap", () => { }, AnimationTimeout); }); - it("auto-computes middle color as a blend between colorbrewer endpoints on first activation", (done) => { + it("middle picker retains a valid color when activateGradientMiddle is first enabled in colorbrewer mode", (done) => { dataView.metadata.objects = { general: { activateGradientMiddle: true, @@ -947,26 +947,6 @@ describe("TableHeatmap", () => { }, AnimationTimeout); }); - it("middle picker is an interpolated blend between the colorbrewer palette endpoints", (done) => { - dataView.metadata.objects = { - general: { - activateGradientMiddle: true, - enableColorbrewer: true, - colorbrewer: "Reds", - buckets: 5 - } - }; - - visualBuilder.updateRenderTimeout(dataView, () => { - const middle = readMiddlePicker(); - expect(middle).toBeTruthy(); - // The auto-computed middle is a linear blend — it must differ from both palette endpoints. - const r5 = colorbrewer.Reds[5]; - expect(areColorsEqual(middle, r5[0])).toBeFalse(); - expect(areColorsEqual(middle, r5[r5.length - 1])).toBeFalse(); - done(); - }, AnimationTimeout); - }); }); describe("utils:getOpacity", () => { From b9ee930d78b37b8610ff322e99e2139dbc58383f Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Mon, 8 Jun 2026 19:16:07 +0500 Subject: [PATCH 12/19] fix: reorder slices in gradientScaleGroup for improved clarity --- src/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.ts b/src/settings.ts index 5ffc598..25e4528 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -470,7 +470,7 @@ export class GeneralSettings extends FormattingSettingsCompositeCard { name: "gradientScaleGroup", displayNameKey: "Visual_General_Additional", collapsible: false, - slices: [this.invertColorScale, this.buckets, this.fillNullValuesCells], + slices: [this.buckets, this.fillNullValuesCells, this.invertColorScale, ], }); public groups: FormattingSettingsGroup[] = [this.paletteGroup, this.gradientGroup, this.gradientScaleGroup]; From 09871f4970544d6d128d604f6480000bced258d0 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Tue, 9 Jun 2026 16:23:47 +0500 Subject: [PATCH 13/19] fix: correct grid height adjustment factor and set max bucket count limit --- src/heatmapUtils.ts | 4 ++-- src/settings.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/heatmapUtils.ts b/src/heatmapUtils.ts index 4d4e4da..ba447d1 100644 --- a/src/heatmapUtils.ts +++ b/src/heatmapUtils.ts @@ -53,7 +53,7 @@ export function getOpacity( } export const YAxisAdditionalMargin: number = 5; -export const AdditionalSpaceForColorbrewerCells: number = 2; +export const GridHeightAdjustmentFactor: number = 2; export const ConstGridMinHeight: number = 5; export const ConstGridMinWidth: number = 1; export const CellMaxHeightLimit: number = 300; @@ -113,7 +113,7 @@ export function calculateGridSizeHeight( ): number { const gridSizeHeight: number = Math.floor( (viewportHeight - marginTop - xAxisHeight - marginBottom - YAxisAdditionalMargin) / - (categoryYLength + AdditionalSpaceForColorbrewerCells) + (categoryYLength + GridHeightAdjustmentFactor) ); return Math.max(ConstGridMinHeight, Math.min(gridSizeHeight, CellMaxHeightLimit)); diff --git a/src/settings.ts b/src/settings.ts index 25e4528..c2a8938 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -631,6 +631,7 @@ export class SettingsModel extends FormattingSettingsModel { 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 From 13aa1e05197ebbcdf22ac5c600ac46d5be06c096 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Wed, 10 Jun 2026 10:54:51 +0500 Subject: [PATCH 14/19] test: update assertions to check for non-empty labelDOMItems in TableHeatmap tests --- test/visualTest.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/visualTest.ts b/test/visualTest.ts index d17fd98..416da35 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -175,7 +175,7 @@ describe("TableHeatmap", () => { const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "20px"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItem).toBeTruthy(); done(); @@ -196,7 +196,7 @@ describe("TableHeatmap", () => { const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "40px"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItem).toBeTruthy(); done(); @@ -217,7 +217,7 @@ describe("TableHeatmap", () => { const filteredItem = items.find(i => getComputedStyle(i)["font-family"] === "Arial"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItem).toBeTruthy(); done(); @@ -240,7 +240,7 @@ describe("TableHeatmap", () => { const filteredItems = items.find(i => getComputedStyle(i)["font-size"] === "12px"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItems).toBeTruthy(); done(); @@ -261,7 +261,7 @@ describe("TableHeatmap", () => { const filteredItems = items.find(i => getComputedStyle(i)["font-size"] === "40px"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItems).toBeTruthy(); done(); @@ -282,7 +282,7 @@ describe("TableHeatmap", () => { const filteredItems = items.find(i => getComputedStyle(i)["font-family"] === "Verdana"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItems).toBeTruthy(); done(); @@ -305,7 +305,7 @@ describe("TableHeatmap", () => { const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "24px"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItem).toBeTruthy(); done(); @@ -326,7 +326,7 @@ describe("TableHeatmap", () => { const filteredItem = items.find(i => getComputedStyle(i)["font-size"] === "40px"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItem).toBeTruthy(); done(); @@ -347,7 +347,7 @@ describe("TableHeatmap", () => { const filteredItem = items.find(i => getComputedStyle(i)["font-family"] === "Verdana"); - expect(labelDOMItems).toBeTruthy(); + expect(labelDOMItems.length).toBeGreaterThan(0); expect(filteredItem).toBeTruthy(); done(); From a71d3bc54eb9f06f72affbb955d2cb6cf1d5adf7 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 11 Jun 2026 12:17:22 +0500 Subject: [PATCH 15/19] refactor: update stroke handling in settings model and adjust visual rendering --- src/heatmapUtils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/heatmapUtils.ts b/src/heatmapUtils.ts index ba447d1..3182c5d 100644 --- a/src/heatmapUtils.ts +++ b/src/heatmapUtils.ts @@ -33,7 +33,7 @@ import { ColorHelper } from "powerbi-visuals-utils-colorutils"; import maxBy from "lodash.maxby"; import { IColorArray, TableHeatMapChartData } from "./dataInterfaces"; -import { BaseLabelCardSettings, colorbrewer, GeneralSettings, SettingsModel, YAxisLabelsSettings } from "./settings"; +import { BaseLabelCardSettings, colorbrewer, SettingsModel, YAxisLabelsSettings } from "./settings"; export const DimmedOpacity: number = 0.4; export const DefaultOpacity: number = 1.0; @@ -135,8 +135,8 @@ export function calculateGridSizeWidth( /** * Returns the start and end colours for the active colour source (colorbrewer palette or - * user-defined gradient). Centralises the fallback logic so it cannot drift between - * `createRenderOptions` and `initColors`. + * 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, @@ -170,10 +170,14 @@ export function parseSettings(colorHelper: ColorHelper, settingsModel: SettingsM 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; - GeneralSettings.stroke = foregroundColor; + settingsModel.general.stroke = foregroundColor; settingsModel.general.textColor = foregroundColor; + } else { + settingsModel.general.stroke = "#E6E6E6"; + settingsModel.general.textColor = "#AAAAAA"; } return settingsModel; From 55de5cab3983026061756b4c058a1e31768b9113 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 11 Jun 2026 12:17:52 +0500 Subject: [PATCH 16/19] fix: update gradient middle color calculation --- src/visual.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/visual.ts b/src/visual.ts index e9c06be..ac20b72 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -75,7 +75,6 @@ import { import { BaseLabelCardSettings, - GeneralSettings, SettingsModel, colorbrewer } from "./settings"; @@ -514,10 +513,9 @@ export class TableHeatMap implements IVisual { } const storedMiddle: string = settingsModel.general.gradientMiddle.value.value; - const midIndex: number = Math.floor((numBuckets - 1) / 2); - const midPos: number = midIndex / (numBuckets - 1); - const middleColor: string = storedMiddle || createLinearColorScale([0, 1], [startColor, endColor], true)(midPos); - const domain: number[] = [0, midIndex, numBuckets - 1]; + const mid: number = (numBuckets - 1) / 2; + const middleColor: string = storedMiddle || createLinearColorScale([0, 1], [startColor, endColor], true)(0.5); + const domain: number[] = [0, mid, numBuckets - 1]; const range: string[] = [startColor, middleColor, endColor]; const colorScale: LinearColorScale = createLinearColorScale(domain, range, true); const colors: string[] = []; @@ -552,7 +550,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) @@ -576,7 +574,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; @@ -773,7 +771,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); From 1c223ea62d88f0718ba0995945a6236d27337fce Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 11 Jun 2026 12:18:19 +0500 Subject: [PATCH 17/19] fix: update formatting settings import --- src/settings.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index c2a8938..031ad0b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -27,6 +27,7 @@ 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; @@ -448,7 +449,7 @@ export class GeneralSettings extends FormattingSettingsCompositeCard { } }); - public static stroke: string = "#E6E6E6"; + public stroke: string = "#E6E6E6"; public textColor: string = "#AAAAAA"; private paletteGroup: FormattingSettingsGroup = new formattingSettings.Group({ @@ -470,7 +471,7 @@ export class GeneralSettings extends FormattingSettingsCompositeCard { name: "gradientScaleGroup", displayNameKey: "Visual_General_Additional", collapsible: false, - slices: [this.buckets, this.fillNullValuesCells, this.invertColorScale, ], + slices: [this.buckets, this.fillNullValuesCells, this.invertColorScale], }); public groups: FormattingSettingsGroup[] = [this.paletteGroup, this.gradientGroup, this.gradientScaleGroup]; @@ -585,7 +586,7 @@ export class SettingsModel extends FormattingSettingsModel { 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 cards: FormattingSettingsCard[] = [this.general, this.labels, this.xAxisLabels, this.yAxisLabels]; public CurrentBucketCount: number = GeneralSettings.BucketCountMinLimit; From 0b3ce30f2f2486cf19f2f08d5e57697547399f93 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Thu, 11 Jun 2026 12:18:28 +0500 Subject: [PATCH 18/19] chore: update CHANGELOG.md to reflect new features, bug fixes, and code quality improvements --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 191ebe8..1ce5eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,18 @@ ### 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 a user-defined or auto-computed midpoint colour. +* 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 From bce7e5669403ac0edcfa23f914d5a77f50d2afe1 Mon Sep 17 00:00:00 2001 From: Ansagan Islamgali Date: Fri, 12 Jun 2026 15:52:30 +0500 Subject: [PATCH 19/19] fix: remove unreachable code blocks --- src/visual.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/visual.ts b/src/visual.ts index ac20b72..dc9ba70 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -157,7 +157,6 @@ 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; @@ -503,18 +502,8 @@ export class TableHeatMap implements IVisual { settingsModel.general.gradientEnd.value.value ); - if (numBuckets < 3) { - const fallbackScale: LinearColorScale = createLinearColorScale([0, Math.max(numBuckets - 1, 1)], [startColor, endColor], true); - const colors: string[] = []; - for (let i: number = 0; i < numBuckets; i++) { - colors.push(fallbackScale(i)); - } - return colors; - } - - const storedMiddle: string = settingsModel.general.gradientMiddle.value.value; + const middleColor: string = settingsModel.general.gradientMiddle.value.value; const mid: number = (numBuckets - 1) / 2; - const middleColor: string = storedMiddle || createLinearColorScale([0, 1], [startColor, endColor], true)(0.5); const domain: number[] = [0, mid, numBuckets - 1]; const range: string[] = [startColor, middleColor, endColor]; const colorScale: LinearColorScale = createLinearColorScale(domain, range, true);