const defaultDark = ['#FF0000', '#00FF00', '#FFFF00', '#FF00FF', '#00FFFF', '#0000FF'];
const defaultLight = ['#FF0000', '#00FF00', '#0000FF', '#FF00FF', '#FF8000', '#FFFF00'];
const gradientDark = ['#9B2800', '#AF5014', '#C36428', '#D7783C', '#EB8C50', '#FFA064'];
const gradientLight = ['#00509B', '#147EAF', '#288CC3', '#3CA0D7', '#50B4EB', '#64C8FF'];
const monochromaticDark = ['#FFFFFF', '#C8C8C8', '#B4B4B4', '#A0A0A0', '#8C8C8C', '#787878'];
const monochromaticLight = ['#141414', '#282828', '#3C3C3C', '#505050', '#646464', '#787878'];
const objectColorMap = [
   '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
   '#FF8000', '#00FF80', '#8000FF', '#FF0080', '#80FF00', '#0080FF'
];
const trackColorMap = [
   '#FF0000', '#B2FF00', '#00FF9D', '#0011FF',
   '#FF00BF', '#FF8C00', '#21FF00', '#00D0FF',
   '#7F00FF', '#FF0033', '#E1FF00', '#00FF6E',
   '#003FFF', '#FF00F2', '#FF5D00', '#50FF00',
   '#41D8D8', '#6E41D8', '#D8417B', '#D8CE41',
   '#41D866', '#4185D8', '#C441D8', '#D85C41',
   '#8CD841', '#41D8BA', '#5241D8', '#D84197',
   '#D8B241', '#41D848', '#41A1D8', '#A841D8'
];
const seriesMarkers = [
   "circle", "triangle", "square", "plus", "diamond", "star"
];

const nisGradients = new Map([
   // QCustomPlot gradients
   //////////////////////////////////////////////////
   ["qcpGrayscale", [[
      [0.00, "black"],
      [1.00, "white"]],
   d3.interpolateRgb]],
   ["qcpHot", [[
      [0.00, "rgb(50, 0, 0)"],
      [0.20, "rgb(180, 10, 0)"],
      [0.40, "rgb(245, 50, 0)"],
      [0.60, "rgb(255, 150, 10)"],
      [0.80, "rgb(255, 255, 50)"],
      [1.00, "rgb(255, 255, 255)"]],
   d3.interpolateRgb]],
   ["qcpCold", [[
      [0.00, "rgb(0, 0, 50)"],
      [0.20, "rgb(0, 10, 180)"],
      [0.40, "rgb(0, 50, 245)"],
      [0.60, "rgb(10, 150, 255)"],
      [0.80, "rgb(50, 255, 255)"],
      [1.00, "rgb(255, 255, 255)"]],
   d3.interpolateRgb]],
   ["qcpNight", [[
      [0.00, "rgb(10, 20, 30)"],
      [1.00, "rgb(250, 255, 250)"]],
   d3.interpolateHsv]],
   ["qcpCandy", [[
      [0.00, "rgb(0, 0, 255)"],
      [1.00, "rgb(255, 250, 250)"]],
   d3.interpolateHsv]],
   ["qcpGeography", [[
      [0.00, "rgb(70, 170, 210)"],
      [0.20, "rgb(90, 160, 180)"],
      [0.25, "rgb(45, 130, 175)"],
      [0.30, "rgb(100, 140, 125)"],
      [0.50, "rgb(100, 140, 100)"],
      [0.60, "rgb(130, 145, 120)"],
      [0.70, "rgb(140, 130, 120)"],
      [0.90, "rgb(180, 190, 190)"],
      [1.00, "rgb(210, 210, 230)"]],
   d3.interpolateRgb]],
   ["qcpIon", [[
      [0.00, "rgb(50, 10, 10)"],
      [0.45, "rgb(0, 0, 255)"],
      [0.80, "rgb(0, 255, 255)"],
      [1.00, "rgb(0, 255, 0)"]],
   d3.interpolateHsv]],
   ["qcpThermal", [[
      [0.00, "rgb(0, 0, 50)"],
      [0.15, "rgb(20, 0, 120)"],
      [0.33, "rgb(200, 30, 140)"],
      [0.60, "rgb(255, 100, 0)"],
      [0.85, "rgb(255, 255, 40)"],
      [1.00, "rgb(255, 255, 255)"]],
   d3.interpolateRgb]],
   ["qcpPolar", [[
      [0.00, "rgb(50, 255, 255)"],
      [0.18, "rgb(10, 70, 255)"],
      [0.28, "rgb(10, 10, 190)"],
      [0.50, "rgb(0, 0, 0)"],
      [0.72, "rgb(190, 10, 10)"],
      [0.82, "rgb(255, 70, 10)"],
      [1.00, "rgb(255, 255, 50)"]],
   d3.interpolateRgb]],
   ["qcpSpectrum", [[
      [0.00, "rgb(50, 0, 50)"],
      [0.15, "rgb(0, 0, 255)"],
      [0.35, "rgb(0, 255, 255)"],
      [0.60, "rgb(255, 255, 0)"],
      [0.75, "rgb(255, 30, 0)"],
      [1.00, "rgb(50, 0, 0)"]],
   d3.interpolateHsv]],
   ["qcpJet", [[
      [0.00, "rgb(0, 0, 100)"],
      [0.15, "rgb(0, 50, 255)"],
      [0.35, "rgb(0, 255, 255)"],
      [0.65, "rgb(255, 255, 0)"],
      [0.85, "rgb(255, 30, 0)"],
      [1.00, "rgb(100, 0, 0)"]],
   d3.interpolateRgb]],
   ["qcpHues", [[
      [0.00, "rgb(255, 0, 0)"],
      [0.33, "rgb(0, 0, 255)"],
      [0.67, "rgb(0, 255, 0)"],
      [1.00, "rgb(255, 0, 0)"]],
   d3.interpolateHsv]],
   // NIS gradients
   //////////////////////////////////////////////////
   ["nisRainbow", [[
      [0.00, "#630596"],
      [0.04, "#520da3"],
      [0.08, "#3f1bae"],
      [0.12, "#2c2db4"],
      [0.16, "#1a43b4"],
      [0.20, "#0c5cae"],
      [0.24, "#0376a2"],
      [0.27, "#008f92"],
      [0.31, "#02a57e"],
      [0.35, "#0ab769"],
      [0.39, "#17c554"],
      [0.43, "#27cd40"],
      [0.47, "#3ad12e"],
      [0.51, "#4ed21f"],
      [0.55, "#62ce13"],
      [0.59, "#76c80a"],
      [0.63, "#8ac004"],
      [0.67, "#9cb600"],
      [0.71, "#aeaa00"],
      [0.75, "#be9d01"],
      [0.78, "#cd8f05"],
      [0.82, "#db810b"],
      [0.86, "#e67214"],
      [0.90, "#f0621f"],
      [0.94, "#f8532c"],
      [0.98, "#fd433b"]],
   d3.interpolateRgb]],
   ["nisIron", [[
      [0.00, "#280000"],
      [0.04, "#360000"],
      [0.08, "#400"],
      [0.12, "#520000"],
      [0.16, "#600000"],
      [0.20, "#6e0000"],
      [0.24, "#7c0000"],
      [0.27, "#8a0000"],
      [0.31, "#960000"],
      [0.35, "#a30000"],
      [0.39, "#af0000"],
      [0.43, "#bc0000"],
      [0.47, "#c70000"],
      [0.51, "#d21300"],
      [0.55, "#d92600"],
      [0.59, "#e13900"],
      [0.63, "#e54c00"],
      [0.67, "#e95e00"],
      [0.71, "#ee7100"],
      [0.75, "#f18400"],
      [0.78, "#f39700"],
      [0.82, "#f5aa00"],
      [0.86, "#f7bd00"],
      [0.90, "#fad049"],
      [0.94, "#fce392"],
      [0.98, "#fef6db"]],
   d3.interpolateRgb
   ]],
   ["nisCyanGreen", [[
      [0.04, "#001717"],
      [0.08, "#002f2f"],
      [0.12, "#004747"],
      [0.16, "#005f5f"],
      [0.20, "#077"],
      [0.24, "#008f8f"],
      [0.27, "#00a28a"],
      [0.31, "#00b272"],
      [0.35, "#00c25a"],
      [0.39, "#00d242"],
      [0.43, "#00e22b"],
      [0.47, "#00f213"],
      [0.51, "#07ff00"],
      [0.55, "#2fff00"],
      [0.59, "#57ff00"],
      [0.63, "#7fff00"],
      [0.67, "#a7ff00"],
      [0.71, "#cfff00"],
      [0.75, "#f7ff00"],
      [0.78, "#ffff1f"],
      [0.82, "#ffff47"],
      [0.86, "#ffff6f"],
      [0.90, "#ffff97"],
      [0.94, "#ffffbf"],
      [0.98, "#ffffe7"]],
   d3.interpolateRgb
   ]],
   ["nisBlueGreen", [[
      [0.04, "#000d1a"],
      [0.08, "#001a35"],
      [0.12, "#00274f"],
      [0.16, "#00356a"],
      [0.20, "#004284"],
      [0.24, "#004f9f"],
      [0.27, "#00649a"],
      [0.31, "#007f7f"],
      [0.35, "#009a64"],
      [0.39, "#00b44a"],
      [0.43, "#00cf2f"],
      [0.47, "#00e915"],
      [0.51, "#07ff00"],
      [0.55, "#2fff00"],
      [0.59, "#57ff00"],
      [0.63, "#7fff00"],
      [0.67, "#a7ff00"],
      [0.71, "#cfff00"],
      [0.75, "#f7ff00"],
      [0.78, "#ffff1f"],
      [0.82, "#ffff47"],
      [0.86, "#ffff6f"],
      [0.90, "#ffff97"],
      [0.94, "#ffffbf"],
      [0.98, "#ffffe7"]],
   d3.interpolateRgb
   ]],
   ["nisBlueRed", [[
      [0.00, "#00001f"],
      [0.04, "#000045"],
      [0.08, "#0d0071"],
      [0.12, "#2b009c"],
      [0.16, "#4900c0"],
      [0.20, "#6800dd"],
      [0.24, "#8600da"],
      [0.27, "#9e00bc"],
      [0.31, "#ad0097"],
      [0.35, "#ba0072"],
      [0.39, "#c9074e"],
      [0.43, "#d61d2a"],
      [0.47, "#e53905"],
      [0.51, "#f35400"],
      [0.55, "#fd6d00"],
      [0.59, "#ff8100"],
      [0.63, "#ff9300"],
      [0.67, "#ffa400"],
      [0.71, "#ffb600"],
      [0.75, "#ffc900"],
      [0.78, "#ffdb00"],
      [0.82, "#ffed08"],
      [0.86, "#fffb42"],
      [0.90, "#ffff90"],
      [0.94, "#ffffdf"],
      [0.98, "#fff"]],
   d3.interpolateRgb
   ]],
   ["nisRedWhite", [[
      [0.04, "#070000"],
      [0.08, "#270000"],
      [0.12, "#470000"],
      [0.16, "#670000"],
      [0.20, "#800"],
      [0.24, "#a80000"],
      [0.27, "#c80000"],
      [0.31, "#e90000"],
      [0.35, "#ff0900"],
      [0.39, "#ff2a00"],
      [0.43, "#ff4a00"],
      [0.47, "#ff6a00"],
      [0.51, "#ff8b00"],
      [0.55, "#ffab00"],
      [0.59, "#ffcb00"],
      [0.63, "#ffec00"],
      [0.67, "#ffff0c"],
      [0.71, "#ffff2d"],
      [0.75, "#ffff4d"],
      [0.78, "#ffff6e"],
      [0.82, "#ffff8e"],
      [0.86, "#ffffae"],
      [0.90, "#ffffce"],
      [0.94, "#ffffef"],
      [0.98, "#fff"]],
   d3.interpolateRgb
   ]],
   ["nisMagenta", [[
      [0.04, "#0f000f"],
      [0.08, "#1f001f"],
      [0.12, "#2f002f"],
      [0.16, "#3f003f"],
      [0.20, "#4f004f"],
      [0.24, "#5f005f"],
      [0.27, "#740074"],
      [0.31, "#8c008c"],
      [0.35, "#a400a4"],
      [0.39, "#bc00bc"],
      [0.43, "#d300d3"],
      [0.47, "#eb00eb"],
      [0.51, "#ff03ff"],
      [0.55, "#ff13ff"],
      [0.59, "#ff23ff"],
      [0.63, "#f3f"],
      [0.67, "#ff42ff"],
      [0.71, "#ff52ff"],
      [0.75, "#ff62ff"],
      [0.78, "#ff79ff"],
      [0.82, "#ff91ff"],
      [0.86, "#ffa8ff"],
      [0.90, "#ffc0ff"],
      [0.94, "#ffd8ff"],
      [0.98, "#fff0ff"]],
   d3.interpolateRgb
   ]],
   ["nisBrown", [[
      [0.00, "#090202"],
      [0.04, "#260b01"],
      [0.08, "#461401"],
      [0.12, "#5d1e01"],
      [0.16, "#6f2701"],
      [0.20, "#7c3101"],
      [0.24, "#893901"],
      [0.27, "#944001"],
      [0.31, "#9b4601"],
      [0.35, "#a34d01"],
      [0.39, "#aa5401"],
      [0.43, "#b25b06"],
      [0.47, "#b8610a"],
      [0.51, "#bd660e"],
      [0.55, "#c26b12"],
      [0.59, "#c77016"],
      [0.63, "#cc751a"],
      [0.67, "#d1791e"],
      [0.71, "#d77e22"],
      [0.75, "#dc8326"],
      [0.78, "#e1882d"],
      [0.82, "#e68d33"],
      [0.86, "#ec943f"],
      [0.90, "#f19d4e"],
      [0.94, "#f6ae70"],
      [0.98, "#fbd8a7"]],
   d3.interpolateRgb
   ]],
   ["nisOrange", [[
      [0.04, "#1d1400"],
      [0.08, "#3b2900"],
      [0.12, "#593d00"],
      [0.16, "#775200"],
      [0.20, "#956600"],
      [0.24, "#b37b00"],
      [0.27, "#d18f00"],
      [0.31, "#efa400"],
      [0.35, "#ffb300"],
      [0.39, "#ffbd00"],
      [0.43, "#ffcf00"],
      [0.47, "#ffcf00"],
      [0.51, "#ffd900"],
      [0.55, "#ffe200"],
      [0.59, "#ffeb00"],
      [0.63, "#fff500"],
      [0.67, "#ff0"],
      [0.71, "#ffff1d"],
      [0.75, "#ffff3b"],
      [0.78, "#ffff59"],
      [0.82, "#ff7"],
      [0.86, "#ffff95"],
      [0.90, "#ffffb3"],
      [0.94, "#ffffd1"],
      [0.98, "#ffffef"]],
   d3.interpolateRgb
   ]],
   ["nisYellow", [[
      [0.04, "#131307"],
      [0.08, "#27270f"],
      [0.12, "#3b3b17"],
      [0.16, "#4f4f1f"],
      [0.20, "#636327"],
      [0.24, "#77772f"],
      [0.27, "#8b8b37"],
      [0.31, "#9f9f3f"],
      [0.35, "#b3b347"],
      [0.39, "#c7c74f"],
      [0.43, "#dbdb57"],
      [0.47, "#efef5f"],
      [0.51, "#ffff67"],
      [0.55, "#ffff6f"],
      [0.59, "#ff7"],
      [0.63, "#ffff7f"],
      [0.67, "#ffff87"],
      [0.71, "#ffff8f"],
      [0.75, "#ffff97"],
      [0.78, "#ffff9f"],
      [0.82, "#ffffa7"],
      [0.86, "#ffffaf"],
      [0.90, "#ffffb7"],
      [0.94, "#ffffbf"],
      [0.98, "#ffffc7"]],
   d3.interpolateRgb
   ]],
   ["nisRainbowContrast", [[
      [0.00, "#00f"],
      [0.04, "#0041ff"],
      [0.08, "#0083ff"],
      [0.12, "#00b5ff"],
      [0.16, "#00d8ff"],
      [0.20, "#00fbff"],
      [0.24, "#00ffde"],
      [0.27, "#00ffb9"],
      [0.31, "#00ff8a"],
      [0.35, "#00ff4b"],
      [0.39, "#00ff0c"],
      [0.43, "#37ff00"],
      [0.47, "#7bff00"],
      [0.51, "#b5ff00"],
      [0.55, "#d5ff00"],
      [0.59, "#f5ff00"],
      [0.63, "#ffe700"],
      [0.67, "#ffc600"],
      [0.71, "#ffa102"],
      [0.75, "#ff6b0e"],
      [0.78, "#ff351b"],
      [0.82, "#ff233f"],
      [0.86, "#ff2a74"],
      [0.90, "#ff30a7"],
      [0.94, "#ff36ca"],
      [0.98, "#ff3ced"]],
   d3.interpolateRgb
   ]],
   ["nisRainbowDark", [[
      [0.04, "#1f0d57"],
      [0.08, "#3f1bae"],
      [0.12, "#2c2db4"],
      [0.16, "#1a43b4"],
      [0.20, "#0c5cae"],
      [0.24, "#0376a2"],
      [0.27, "#008f92"],
      [0.31, "#02a57e"],
      [0.35, "#0ab769"],
      [0.39, "#17c554"],
      [0.43, "#27cd40"],
      [0.47, "#3ad12e"],
      [0.51, "#4ed21f"],
      [0.55, "#62ce13"],
      [0.59, "#76c80a"],
      [0.63, "#8ac004"],
      [0.67, "#9cb600"],
      [0.71, "#aeaa00"],
      [0.75, "#be9d01"],
      [0.78, "#cd8f05"],
      [0.82, "#db810b"],
      [0.86, "#e67214"],
      [0.90, "#f0621f"],
      [0.94, "#f8532c"],
      [0.98, "#fd433b"]],
   d3.interpolateRgb
   ]],
   ["nisRedGreenScale", [[
      [0.00, "red"],
      [0.04, "#ea0000"],
      [0.08, "#d60000"],
      [0.12, "#c20000"],
      [0.16, "#ae0000"],
      [0.20, "#9a0000"],
      [0.24, "#860000"],
      [0.27, "#720000"],
      [0.31, "#5e0000"],
      [0.35, "#4a0000"],
      [0.39, "#360000"],
      [0.43, "#200"],
      [0.47, "#0e0000"],
      [0.51, "#000600"],
      [0.55, "#001a00"],
      [0.59, "#002e00"],
      [0.63, "#004200"],
      [0.67, "#005600"],
      [0.71, "#006a00"],
      [0.75, "#007e00"],
      [0.78, "#009200"],
      [0.82, "#00a600"],
      [0.86, "#00ba00"],
      [0.90, "#00ce00"],
      [0.94, "#00e200"],
      [0.98, "#00f600"]],
   d3.interpolateRgb
   ]],
   ["nisRedGreenBinary", [[
      [0.50, "red"],
      [0.5001, "green"]],
   d3.interpolateRgb
   ]],
   ["nisAltitude", [[
      [0.00, "#50dd39"],
      [0.04, "#50dd39"],
      [0.08, "#52da37"],
      [0.12, "#57d132"],
      [0.16, "#5cc62b"],
      [0.20, "#62b922"],
      [0.24, "#69ab18"],
      [0.27, "#719d0f"],
      [0.31, "#788f06"],
      [0.35, "#808200"],
      [0.39, "#870"],
      [0.43, "#906f00"],
      [0.47, "#986a00"],
      [0.51, "#a06900"],
      [0.55, "#a86d0b"],
      [0.59, "#af741a"],
      [0.63, "#b87e2d"],
      [0.67, "#c08b43"],
      [0.71, "#c9995b"],
      [0.75, "#d2a875"],
      [0.78, "#dbb88e"],
      [0.82, "#e3c8a8"],
      [0.86, "#ead8c0"],
      [0.90, "#f1e5d6"],
      [0.94, "#f7f1e9"],
      [0.98, "#fdfbf8"]],
   d3.interpolateRgb
   ]],
   ["nisMagentaHot", [[
      [0.00, "#e2007a"],
      [0.04, "#e3067d"],
      [0.08, "#e30c80"],
      [0.12, "#e41484"],
      [0.16, "#e51c89"],
      [0.20, "#e6268e"],
      [0.24, "#e73093"],
      [0.27, "#e93a98"],
      [0.31, "#ea459e"],
      [0.35, "#eb51a4"],
      [0.39, "#ed5dab"],
      [0.43, "#ee69b1"],
      [0.47, "#ef76b8"],
      [0.51, "#f182be"],
      [0.55, "#f28ec4"],
      [0.59, "#f49bcb"],
      [0.63, "#f5a7d1"],
      [0.67, "#f6b3d7"],
      [0.71, "#f8bedd"],
      [0.75, "#f9c9e3"],
      [0.78, "#fad3e8"],
      [0.82, "#fbdded"],
      [0.86, "#fce6f2"],
      [0.90, "#fdeef6"],
      [0.94, "#fef6fa"],
      [0.98, "#fffcfd"]],
   d3.interpolateRgb
   ]],
   ["nisRedHotX", [[
      [0.00, "red"],
      [0.04, "#ff0c00"],
      [0.08, "#ff1c00"],
      [0.12, "#ff3000"],
      [0.16, "#ff4500"],
      [0.20, "#ff5d00"],
      [0.24, "#ff7600"],
      [0.27, "#ff8e00"],
      [0.31, "#ffa700"],
      [0.35, "#ffbe00"],
      [0.39, "#ffd300"],
      [0.43, "#ffe600"],
      [0.47, "#fff600"],
      [0.51, "#ffff02"],
      [0.55, "#ffff0f"],
      [0.59, "#ffff20"],
      [0.63, "#ffff34"],
      [0.67, "#ffff4a"],
      [0.71, "#ffff62"],
      [0.75, "#ffff7b"],
      [0.78, "#ffff93"],
      [0.82, "#ffffac"],
      [0.86, "#ffffc3"],
      [0.90, "#ffffd7"],
      [0.94, "#ffffea"],
      [0.98, "#fffff8"]],
   d3.interpolateRgb
   ]],
   ["nisSunnyDay", [[
      [0.00, "#009ee0"],
      [0.04, "#06a0dc"],
      [0.08, "#0ca2d6"],
      [0.12, "#14a4cf"],
      [0.16, "#1ca7c8"],
      [0.20, "#26a9bf"],
      [0.24, "#30adb7"],
      [0.27, "#3ab0ad"],
      [0.31, "#45b3a3"],
      [0.35, "#51b799"],
      [0.39, "#5dbb8f"],
      [0.43, "#69be84"],
      [0.47, "#76c279"],
      [0.51, "#82c66e"],
      [0.55, "#8eca63"],
      [0.59, "#9bce58"],
      [0.63, "#a7d14e"],
      [0.67, "#b3d543"],
      [0.71, "#bed839"],
      [0.75, "#c9dc2f"],
      [0.78, "#d3df26"],
      [0.82, "#dde21e"],
      [0.86, "#e6e516"],
      [0.90, "#eee70f"],
      [0.94, "#f6ea08"],
      [0.98, "#fcec03"]],
   d3.interpolateRgb
   ]]
]);

/*___________________________________________________________________________*/
function integerToRGB(num) {
   num >>>= 0;
   var a = num & 0xFF / 255,
      b = (num & 0xFF00) >>> 8,
      g = (num & 0xFF0000) >>> 16,
      r = ((num & 0xFF000000) >>> 24);
   //return "rgba(" + [r, g, b, a].join(",") + ")";
   return "rgb(" + [r, g, b].join(",") + ")";
}

/*___________________________________________________________________________*/
function glyphPalette(palette) {
   const r = document.querySelector(":root");
   var rs = getComputedStyle(r);
   const scheme = rs.getPropertyValue("--color-scheme").trim();
   const isLightScheme = scheme.includes('light');

   if (isLightScheme) {
      switch (palette) {
         case 'default':
         default:
            return Bokeh.Palettes.Light9.map(color => integerToRGB(color)); //defaultDark;
         //return Bokeh.Palettes.Category10_10;//.map(color => integerToRGB(color));
         case 'simple':
            return defaultLight;
         case 'gradient':
            return gradientLight;
         case 'monochromatic':
            return monochromaticLight;
         case 'object':
            return objectColorMap;
         case 'track':
            return trackColorMap;
      }
   }
   else {
      switch (palette) {
         case 'default':
         default:
            return Bokeh.Palettes.Light9.map(color => integerToRGB(color));
         //return Bokeh.Palettes.Category10_10;
         case 'simple':
            return defaultDark;
         case 'gradient':
            return gradientDark;
         case 'monochromatic':
            return monochromaticDark;
         case 'object':
            return objectColorMap;
         case 'track':
            return trackColorMap;
      }
   }
}

/*___________________________________________________________________________*/
class Colorizer {
   #palette
   #palette_by
   #shift
   #color_axis

   constructor(palette, palette_by, color_axis) {
      this.#palette = glyphPalette(palette);
      this.#palette_by = palette_by;
      this.#shift = 0;
      this.#color_axis = color_axis;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   increaseShift(val) {
      this.#shift += val;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   color(column, group, group_count) {
      column += this.#shift;
      let index = 0;
      switch (this.#palette_by) {
         case "group":
            index = group;
            break;
         case "column":
            index = column;
            break;
         default:
            index = column * group_count + group;
      }
      return this.#palette[index % this.#palette.length];
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   marker(column, group, group_count) {
      column += this.#shift;
      let index = 0;
      switch (this.#palette_by) {
         case "group":
            index = column;
            break;
         case "column":
            index = group;
            break;
         default:
            index = Math.floor((column * group_count + group) / this.#palette.length);
      }
      return seriesMarkers[index % seriesMarkers.length];
   }
   
   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   color_axis_palette256() {
      let color256 = [];
      switch (this.#color_axis?.palette ?? 'bkhPlasma') {
         case "bkhInferno":
            color256 = Bokeh.Palettes.Inferno256.map(value => integerToRGB(value));
            break;
         case "bkhMagma":
            color256 = Bokeh.Palettes.Magma256.map(value => integerToRGB(value));
            break;
         case "bkhPlasma":
            color256 = Bokeh.Palettes.Plasma256.map(value => integerToRGB(value));
            break;
         case "bkhViridis":
            color256 = Bokeh.Palettes.Viridis256.map(value => integerToRGB(value));
            break;
         case "bkhCividis":
            color256 = Bokeh.Palettes.Cividis256.map(value => integerToRGB(value));
            break;
         case "bkhTurbo":
            color256 = Bokeh.Palettes.Turbo256.map(value => integerToRGB(value));
            break;
         default: {
            let grad256;
            if(this.#color_axis.palette == "custom") {
               let colors = this.#color_axis?.custom_palette ?? [];
               if(!colors.length || (this.#color_axis?.custom_palette_specific ?? false))
                  colors = [[0.00, "black"]];
               grad256 = [colors, d3.interpolateRgb];
            }
            else
               grad256 =  nisGradients.get(this.#color_axis.palette);
            let interpolation = grad256[1];
            let domain = grad256[0].map(item => item[0]);
            let range = grad256[0].map(item => d3.rgb(item[1]));
            let scale = d3.scaleLinear()
               .domain(domain)
               .range(range)
               .interpolate(interpolation);
            for (let i = 0; i < 256; i++)
               color256.push(scale(i / 255.0));
         }
      }
      if(this.#color_axis?.range_reversed ?? false)
         color256.reverse();
      return color256;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   specific_colors() {
      if((this.#color_axis?.palette ?? 'bkhPlasma') !== 'custom' || !(this.#color_axis?.custom_palette_specific ?? false))
         return {};
      return Object.fromEntries(this.#color_axis?.custom_palette?.map(item => [item[2], item[1]]) ?? []);
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get color_axis_is_linear() { return (this.#color_axis?.range_scale ?? 'linear') == 'linear'; }
   get color_axis_title_visible() { return this.#color_axis?.title_visible ?? true; }
   get color_axis_title_value() { return this.#color_axis?.title_value ?? ''; }
   
}

/*___________________________________________________________________________*/
class LimGraphData extends LimTableDataClient {
   #containerGraph
   #containerToolbar
   #containerMessage

   #tableRowVisibility

   #rowSelection
   #onRowSelectionChanged
   #dataSelectionEnabled

   #blockSetRowSelection
   #blockUpdateRowSelection

   #frameSpan

   constructor(...args) {
      super(...args);
      const [pars, ...remainingArgs] = args;

      this.#containerGraph = document.createElement('div');
      this.#containerGraph.style.flex = "1 1 100%";

      this.#containerMessage = document.createElement('div');

      this.#containerToolbar = document.createElement('div');
      this.#containerToolbar.style.flex = "0 0 20px";
      this.#containerToolbar.style.display = "flex";
      this.#containerToolbar.style.flexDirection = "column";
      this.#containerToolbar.style.height = "100%";
      this.#containerToolbar.style.backgroundColor = "var(--color-window)"
      this.#containerToolbar.classList.add("lim-toolbar");

      this.#tableRowVisibility = pars?.tableRowVisibility ?? "all";

      this.#onRowSelectionChanged = null;
      this.#rowSelection = [];
      this.#dataSelectionEnabled = pars?.dataSelectionEnabled ?? false;

      this.#blockSetRowSelection = false;
      this.#blockUpdateRowSelection = false;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   onTableChanged() {
      this.fetchAndUpdateTable();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   onCurrentLoopIndexesChanged(val, change) {
      if (this.#blockUpdateRowSelection)
         return;
      if (this.isSelectedRowVisiblityIncludingAnyOfLoops(change)) {
         this.fetchAndUpdateTable();
      }
      else if (this.#tableRowVisibility === "currentFrame") {
         this.update();
      }
      else if (this.#dataSelectionEnabled && this?.tableData?.isGroupedFrameTable && val) {
         const rowIndex = this.tableData.loopIndexesToRowIndex(val);
         if (0 <= rowIndex) {
            if (this instanceof LimGraphLinechartV2)
               this.setFrameSpanPosition(rowIndex);
            else {
               this.#blockUpdateRowSelection = true;
               this.rowSelection = [rowIndex];
               this.#blockUpdateRowSelection = false;
            }
         }
      }
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   onCurrentObjectSelectionChanged(val) {
      if (this.#blockUpdateRowSelection)
         return;
      if (this.#dataSelectionEnabled && this?.tableData?.isObjectTable) {
         this.#blockUpdateRowSelection = true;
         this.rowSelection = this.tableData.objectSelectionToRowIndexes(val);
         this.#blockUpdateRowSelection = false;
      }
   }

   onSelectedRowVisibilityChanged(value, old_value) {
      this.fetchAndFillGraphDataSource();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async fetchAndUpdateTable() {
      await this.fetchTableMetadata(() => {
         this?.update?.();
      });
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async fetchAndFillGraphDataSource() {
      if(!this.tableData)
         return;
      let cols = [... new Set(this.tableData.systemColIdList.concat(this.tableData.groupedBy, this?.dataColumns ?? [])).values()];
      const rowFilterList = this.makeDefaultRowFilterList().concat(this?.rowFilter ?? []);
      if(this.constructor.name == "LimGraphFitplot")
         for (let i = 0; i < cols.length; i++) {
            let meta = this.tableData.colMetadata(cols[i]);
            if ('xColId' in meta)
               cols.push(meta['xColId'])
            if ('yColId' in meta) {
               cols.push(meta['yColId']);

               const y_id = meta['yColId'];
               let err_idx = [
                  this.tableData.colMetadataList.findIndex(meta => meta?.aggregated?.startsWith?.("StErr") && meta?.duplicatedFrom === y_id),
                  this.tableData.colMetadataList.findIndex(meta => meta?.aggregated?.startsWith?.("StDev") && meta?.duplicatedFrom === y_id)
               ].find(item => 0 <= item);
               if (0 <= err_idx)
                  cols.push(this.tableData.colIdAt(err_idx))
            }
         }
      let pars = {
         cols: JSON.stringify(cols),
         filter: JSON.stringify({ op: "all", filters: rowFilterList })
      };
      await this.fetchTableData(pars, () => {
         this.updateGraph();
      });
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get containerGraph() {
      return this.#containerGraph
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get containerToolbar() {
      return this.#containerToolbar
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get rowSelection() {
      return [...this.#rowSelection];
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set rowSelection(val) {
      if(typeof val === "number")
         val = [val];

      if(this.#blockSetRowSelection || !Array.isArray(this.#rowSelection))
         return;

      this.#rowSelection = [...val];
      this.#blockSetRowSelection = true;
      this?.onRowSelectionChanged?.(this.#rowSelection);
      this.#blockSetRowSelection = false;

      if (!this.#dataSelectionEnabled || this.#blockUpdateRowSelection)
         return;

      this.#blockSetRowSelection = true;
      if (this?.tableData?.isObjectTable) {
         this.currentObjectSelection = this.tableData.rowIndexesToObjectSelection(this.#rowSelection);
      }
      else if (this?.tableData?.isGroupedFrameTable && ["all", "currentTime", "currentWell"].indexOf(this.#tableRowVisibility) !== -1) {
         this.currentLoopIndexes = this.tableData.rowIndexToLoopIndexes(this.#rowSelection);
      }
      this.#blockSetRowSelection = false;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set blockSetRowSelection(val) {
      this.#blockSetRowSelection = val;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get frameSpan() {
      return this.#frameSpan;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set frameSpan(val) {
      if(val === this.#frameSpan)
         return;
      this.#frameSpan = val;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setFrameSpanPosition(row) {
      if(!this.#frameSpan || !this.tableData)
         return;
      let td = this.tableData;
      let x_id = td.colIndexById(this.xAxisColId);
      if(x_id >= 0 && td.rowCount > row)
         this.#frameSpan.location = td.colDataAt(x_id)[row]
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createFrameSpan(fig, button_frame, grouped_buttons)
   {
      if (!(this instanceof LimGraphLinechartV2))
         return false;

      if (!this.tableData.isGroupedFrameTable)
         return false;

      // let timelapse_id;
      // let td = this.tableData;
      // for (let i = 0; i < td.colCount; i++) {
      //    let meta = td.colMetadataAt(i);
      //    if (meta.loopName && "TimeLapse" == meta.loopName) {
      //       timelapse_id = i;
      //       break;
      //    }
      // }
      // if (typeof timelapse_id === 'undefined')
      //    return false;

      const { color_base, color_text, color_midlight, color_highlight, color_accent, color_graph_data, color_graph_highlight } = this.styles();

      var frame_selection_source = new Bokeh.ColumnDataSource({
         data: { x: [NaN], y: [NaN], x1: [NaN], y1: [NaN], label: [''] }
      });
      fig.add_layout(new Bokeh.LabelSet({
         x: { field: 'x1', },
         y: { field: 'y1', },
         x_offset: 0,
         source: frame_selection_source,
         text: { field: 'label', },
         text_outline_color: color_base,
         text_color: color_text,
         text_font: this.fontFamily,
         text_font_style: 'bold',
         text_font_size: '10pt',
         text_line_height: 1
      }));
      fig.segment({
         x0: { field: 'x', },
         y0: { field: 'y', },
         x1: { field: 'x1', },
         y1: { field: 'y1', },
         source: frame_selection_source,
         line_width: 1,
         line_color: color_accent,
         line_dash: 'dotted'
      });
      let frame_selection_span = new Bokeh.Span({
         dimension: "height",
         line_dash: "solid",
         line_width: 1,
         line_color: color_accent,
         level: 'guide'
      });
      fig.add_layout(frame_selection_span);

      let td = this.tableData;
      if(!td || !td.rowCount)
         return false;

      const x_index = this.tableData.colIndexById(this.xAxisColId);
      if(x_index < 0)
         return false;

      const x_values = this.tableData.colDataAt(x_index);
      if(!x_values.length)
         return false;

      const span_loc = x_values[td.loopIndexesToRowIndex(this.currentLoopIndexes)]

      this.frameSpan = new Bokeh.Span({
         location: span_loc,
         dimension: "height",
         level: 'underlay',
         line_width: 1,
         line_color: color_text,
         line_dash: 'dotted'
      });
      fig.add_layout(this.frameSpan);
      if(!this.#dataSelectionEnabled)
         this.frameSpan.visible = false;

      const frame_titles = td.frameTitlesLong;

      let event_tap = new Bokeh.CustomJS({
         args: {
            button: button_frame,
            x_values: x_values,
            that: this,
            fn: (button, x, x_values, that) => {
               if (!button.pressed)
                  return;
               let curr_value = x_values[0];
               let curr_row = 0;
               for (let i = 1; i < x_values.length; i++) {
                  let val = x_values[i];
                  if (Math.abs(x - val) < Math.abs(x - curr_value)) {
                     curr_value = val;
                     curr_row = i;
                  }
               }
               that.currentLoopIndexes = that.tableData.rowIndexToLoopIndexes(curr_row);
            }
         },
         code: "fn(button, this.x, x_values, that);"
      });
      if ('tap' in fig.js_event_callbacks)
         fig.js_event_callbacks["tap"].push(event_tap);
      else
         fig.js_event_callbacks["tap"] = [event_tap];

      let event_hover = new Bokeh.CustomJS({
         args: {
            button: button_frame,
            data_source: frame_selection_source,
            span: frame_selection_span,
            x_values: x_values,
            frame_titles: frame_titles,
            fn: (button, x, y, x_values, frame_titles, data_source, span) => {
               if (!button.pressed)
                  return;
               let curr_value = x_values[0];
               let curr_title = frame_titles[0];
               for (let i = 1; i < x_values.length; i++) {
                  let val = x_values[i];
                  if (Math.abs(x - val) < Math.abs(x - curr_value)) {
                     curr_value = val;
                     curr_title = frame_titles[i];
                  }
               }
               data_source.data = {
                  x: [curr_value], y: [y], x1: [x], y1: [y],
                  label: [curr_title + "\nx: " + curr_value]
               }
               span.location = curr_value;
            }
         },
         code: "fn(button, this.x, this.y, x_values, frame_titles, data_source, span);"
      });
      if ('mousemove' in fig.js_event_callbacks)
         fig.js_event_callbacks["mousemove"].push(event_hover);
      else
         fig.js_event_callbacks["mousemove"] = [event_hover];

      button_frame.onclick = (e, that) => {
         button_frame.togglePressed();
         if (!button_frame.pressed) {
            frame_selection_source.data = {
               x: [NaN], y: [NaN],
               x1: [NaN], y1: [NaN],
               visible: [false], label: [""]
            }
            frame_selection_span.location = NaN;
         }
         else {
            grouped_buttons.forEach((button) => {
               if (button.pressed)
                  button.onclick();
            })
         }
      }

      return true;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   axisMetadata(pars) {
      let col_ids = pars?.col_ids ?? [];
      let axis_settings = pars?.axis_settings ?? LimGraphAxis({});
      let autoscale = pars?.autoscale ?? false;
      let data_min = pars?.data_min ?? null;
      let data_max = pars?.data_max ?? null;
      let is_categorical = pars?.is_categorical ?? false;

      const isArray = Array.isArray(col_ids);
      if (!isArray)
         col_ids = [col_ids];

      let meta = [];
      for (let i = 0; i < col_ids.length; i++)
         meta.push(this.tableData ? this.tableData.colMetadata(col_ids[i]) : undefined);

      let min = data_min;
      let max = data_max;

      if (!is_categorical) {
         if (!autoscale)
            for (let i = 0; i < meta.length; i++) {
               const loc_min = meta[i].globalRange?.min ?? data_min;
               const loc_max = meta[i].globalRange?.max ?? data_max;
               min = min ? Math.min(min, loc_min) : loc_min;
               max = max ? Math.max(max, loc_max) : loc_max;
            }

         let axis_min = axis_settings.range_min_type == 'auto' ? null : axis_settings.range_min_value;
         let axis_max = axis_settings.range_max_type == 'auto' ? null : axis_settings.range_max_value;
         min = axis_min ?? min;
         max = axis_max ?? max;
      }

      let title = '';
      if (axis_settings.title_visible) {
         if (axis_settings.title_value && axis_settings.title_value.length)
            title = axis_settings.title_value;
         else if (meta.length >= 1)
            title = meta[0].units ? `${meta[0].title} [${meta[0].units}]` : `${meta[0].title}`;
      }

      if (!isArray)
         meta = meta[0];

      return { meta, min, max, title };
   }
}

/*___________________________________________________________________________*/
class LimGraphStyle extends LimGraphData {

   #graphTools

   #legendVisibility
   #legendHorizontalAlignment
   #legendVerticalAlignment
   #legendOrientation
   #legendMargin
   #legendPadding
   #legendSpacing
   #legendClickPolicy
   #legendBorderLineColor
   #legendBorderLineAlpha
   #legendBorderLineWidth
   #legendBorderLineDash
   #legendBackgroundFillColor
   #legendBackgroundFillAlpha
   #legendGlyphWidth
   #legendGlyphHeight
   #legendLabelTextColor
   #legendLabelTextAlpha
   #legendLabelTextFont
   #legendLabelTextFontSize
   #legendLabelTextFontStyle

   constructor(pars, ...args) {
      super(pars, ...args);

      this.name = pars?.name;
      this.title = pars?.title;
      this.iconres = pars?.iconres;

      this.#graphTools = LimGraphBokeh.tryArray(pars?.graphTools) ?? LimGraphBokeh.tryParseArray(pars?.graphTools);

      this.#legendVisibility = pars?.legendVisibility ?? "";
      this.#legendHorizontalAlignment = pars?.legendHorizontalAlignment ?? "";
      this.#legendVerticalAlignment = pars?.legendVerticalAlignment ?? "";
      this.#legendOrientation = pars?.legendOrientation ?? "";
      this.#legendMargin = LimGraphBokeh.tryNumber(pars?.legendMargin);
      this.#legendPadding = LimGraphBokeh.tryNumber(pars?.legendPadding);
      this.#legendSpacing = LimGraphBokeh.tryNumber(pars?.legendSpacing);
      this.#legendClickPolicy = pars?.legendClickPolicy ?? "";
      this.#legendBorderLineColor = pars?.legendBorderLineColor ?? "";
      this.#legendBorderLineAlpha = LimGraphBokeh.tryNumber(pars?.legendBorderLineAlpha);
      this.#legendBorderLineWidth = LimGraphBokeh.tryNumber(pars?.legendBorderLineWidth);
      this.#legendBorderLineDash = pars?.legendBorderLineDash ?? "";
      this.#legendBackgroundFillColor = pars?.legendBackgroundFillColor ?? "";
      this.#legendBackgroundFillAlpha = LimGraphBokeh.tryNumber(pars?.legendBackgroundFillAlpha);
      this.#legendGlyphWidth = LimGraphBokeh.tryNumber(pars?.legendGlyphWidth);
      this.#legendGlyphHeight = LimGraphBokeh.tryNumber(pars?.legendGlyphHeight);
      this.#legendLabelTextColor = pars?.legendLabelTextColor ?? "";
      this.#legendLabelTextAlpha = LimGraphBokeh.tryNumber(pars?.legendLabelTextAlpha);
      this.#legendLabelTextFont = pars?.legendLabelTextFont ?? "";
      this.#legendLabelTextFontSize = pars?.legendLabelTextFontSize ?? "";
      this.#legendLabelTextFontStyle = pars?.legendLabelTextFontStyle ?? "";
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get graphTools() {
      let tools = this.#graphTools ?? this.defaultGraphTools;
      const bokeh_tools = Bokeh.Tool.prototype._known_aliases;
      tools = tools.filter(value => bokeh_tools.has(value));
      return tools;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get graphToolsAll() {
      return this.#graphTools ?? this.defaultGraphTools;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get legendVisible() {
      return ["above", "below", "inside", "left", "right"].includes(this.#legendVisibility);
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get legendOutsidePlacement() {
      return ["above", "below", "left", "right"].includes(this.#legendVisibility) ? this.#legendVisibility : null;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get legendLocation() {
      const h = ["left", "center", "right"].includes(this.#legendHorizontalAlignment) ? this.#legendHorizontalAlignment : "right";
      const v = ["top", "center", "bottom"].includes(this.#legendVerticalAlignment) ? this.#legendVerticalAlignment : "top";
      const ret = `${v}_${h}`; //`
      return ret === "center_center" ? "center" : ret;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get legendOrientation() {
      if (this.#legendOrientation !== "")
         return this.#legendOrientation;

      if (["above", "below"].includes(this.legendOutsidePlacement))
         return "horizontal";

      return "vertical";
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get legendVisibility() {
      return this.#legendVisibility;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set legendVisibility(val) {
      if (this.#legendVisibility === val)
         return;

      this.#legendVisibility = val;
      this?.update?.();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get fontFamily() {
      return "Tahoma";
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   scheme() {
      const r = document.querySelector(":root");
      var rs = getComputedStyle(r);
      return rs.getPropertyValue("--color-scheme").trim();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   isLightColorScheme() {
      return this.scheme().includes('light');
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   isDarkColorScheme() {
      return this.scheme().includes('dark');
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styles() {
      const r = document.querySelector(":root");
      var rs = getComputedStyle(r);
      return {
         color_base: rs.getPropertyValue("--color-base"),
         color_text: rs.getPropertyValue("--color-text"),
         color_mid: rs.getPropertyValue("--color-mid"),
         color_midlight: rs.getPropertyValue("--color-midlight"),
         color_highlight: rs.getPropertyValue("--color-highlight"),
         color_accent: rs.getPropertyValue("--color-accent"),
         color_graph_data: rs.getPropertyValue("--color-graph-data"),
         color_graph_highlight: rs.getPropertyValue("--color-graph-highlight")
      };
   };

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   selection_styles() {
      return {
         color_graph_default: Bokeh.Palettes.Category10_3[0],
         color_graph_selection: "#88addd",
         color_graph_nonselection: this.isLightColorScheme() ? "#797979" : "#c7c7c7"
      };
   };

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styles_tooltip() {
      const r = document.querySelector(":root");
      var rs = getComputedStyle(r);
      return {
         color_tooltip: rs.getPropertyValue("--color-tooltip"),
         color_tooltiptext: rs.getPropertyValue("--color-tooltiptext")
      }
   };


   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   limSetColorScheme(val) {
      if (this.fig)
         this.styleGraphFigure(this.fig);
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styleGraphLegend(legend) {
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();

      legend.margin = isNaN(this.#legendMargin) ? 8 : this.#legendMargin;
      legend.padding = isNaN(this.#legendPadding) ? 8 : this.#legendPadding;
      legend.spacing = isNaN(this.#legendSpacing) ? 3 : this.#legendSpacing;
      legend.click_policy = this.#legendClickPolicy === "" ? "none" : this.#legendClickPolicy;

      legend.border_line_color = this.#legendBorderLineColor === "" ? color_midlight : this.#legendBorderLineColor;
      legend.border_line_alpha = isNaN(this.#legendBorderLineAlpha) ? 0.5 : this.#legendBorderLineAlpha;
      legend.border_line_width = isNaN(this.#legendBorderLineWidth) ? 1 : this.#legendBorderLineWidth;
      legend.border_line_dash = this.#legendBorderLineDash === "" ? "solid" : this.#legendBorderLineDash;

      legend.background_fill_color = this.#legendBackgroundFillColor === "" ? color_base : this.#legendBackgroundFillColor;
      legend.background_fill_alpha = isNaN(this.#legendBackgroundFillAlpha) ? 0.5 : this.#legendBackgroundFillAlpha;

      legend.glyph_width = isNaN(this.#legendGlyphWidth) ? 20 : this.#legendGlyphWidth;
      legend.glyph_height = isNaN(this.#legendGlyphHeight) ? 16 : this.#legendGlyphHeight;

      legend.label_text_color = this.#legendLabelTextColor === "" ? color_text : this.#legendLabelTextColor;
      legend.label_text_alpha = isNaN(this.#legendLabelTextAlpha) ? 1.0 : this.#legendLabelTextAlpha;
      legend.label_text_font = this.#legendLabelTextFont === "" ? this.fontFamily : this.#legendLabelTextFont;
      legend.label_text_font_size = this.#legendLabelTextFontSize === "" ? "11px" : this.#legendLabelTextFontSize;
      legend.label_text_font_style = this.#legendLabelTextFontStyle === "" ? "normal" : this.#legendLabelTextFontStyle;

      legend.spacing = 1;
      legend.line_height = 0.1;
      //legend.label_width = 0;
      legend.label_height = 0;
      legend.padding = 5;
      legend.margin = 10;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   measureLegendLabelWidth(text) {
      let obj = document.createElement('div');
      obj.style = 'position: absolute; float: left; white-space: nowrap; visibility: hidden; font: 11px Tahoma;';
      obj.innerHTML = text;
      document.body.append(obj);
      let w = obj.clientWidth;
      obj.remove();
      return w;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styleGraphGrid(grid) {
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();

      grid.grid_line_color = color_midlight;
      grid.minor_grid_line_color = color_midlight;
      grid.minor_grid_line_alpha = 0.5;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styleGraphGridSecondary(grid) {
      this.styleGraphGrid(grid);
      grid.grid_line_dash = 'dotted';
      grid.minor_grid_line_dash = 'dotted';
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styleGraphAxis(axis, axisLabel) {
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();

      axis.axis_label = axisLabel ?? null;
      axis.axis_label_text_font = this.fontFamily;
      axis.axis_label_text_color = color_text;
      axis.axis_label_text_font_style = "normal";
      axis.axis_line_color = color_text;
      axis.major_tick_line_color = color_text;
      axis.major_label_text_font = this.fontFamily;
      axis.major_label_text_color = color_text;
      axis.minor_tick_line_color = color_text;
      axis.minor_tick_line_alpha = 0.4;

      axis.group_text_font = this.fontFamily;
      axis.group_text_font_size = "10px";
      axis.subgroup_text_font = this.fontFamily;
      axis.subgroup_text_font_size = "10px";
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styleGraphAxisCategory(axis) {
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();

      axis.major_label_orientation = (75 / 180) * Math.PI;

      axis.group_text_font = this.fontFamily;
      axis.group_text_color = color_text;
      axis.group_text_font_style = "bold";

      axis.subgroup_text_font = this.fontFamily;
      axis.subgroup_text_color = color_text;
      axis.subgroup_text_font_style = "normal";
      axis.subgroup_label_orientation = (-75 / 180) * Math.PI;
      axis.subgroup_text_align = "right";

      axis.separator_line_color = color_midlight;
      axis.separator_line_dash = "dotted"
      axis.separator_line_width = 1;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   styleGraphFigure(fig, xaxislabel, yaxislabel) {
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();

      fig.border_fill_color = color_base;
      fig.background_fill_color = color_base;

      fig.title.standoff = 20;
      fig.title.align = "center";
      fig.title.background_fill_color = color_base;
      fig.title.text_font = this.fontFamily;
      fig.title.text_color = color_text;
      fig.title.text_font_size = "16px";


      this.styleGraphAxis(fig.xaxis, xaxislabel);
      this.styleGraphAxis(fig.yaxis, yaxislabel);

      this.styleGraphGrid(fig.xgrid);
      this.styleGraphGrid(fig.ygrid);

      this.styleGraphLegend(fig.legend);

      let panTool = null;
      let zoomTool = null;
      for (let tool of fig.toolbar.tools) {
         if (tool instanceof Bokeh.PanTool)
            panTool = tool;
         else if (tool instanceof Bokeh.WheelZoomTool)
            zoomTool = tool;
      }

      fig.toolbar.logo = null;
      fig.toolbar.active_drag = panTool;
      fig.toolbar.active_inspect = null;
      fig.toolbar.active_multi = null;
      fig.toolbar.active_scroll = zoomTool;
      fig.toolbar.active_tap = null;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   hideGraphGrid(fig) {
      fig.xgrid.grid_line_width = 0;
      fig.xgrid.minor_grid_line_width = 0;
      fig.ygrid.grid_line_width = 0;
      fig.ygrid.minor_grid_line_width = 0;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   hideGraphGridX(fig) {
      fig.xgrid.grid_line_width = 0;
      fig.xgrid.minor_grid_line_width = 0;
   }
}

/*___________________________________________________________________________*/
class LimGraphControls extends LimGraphStyle {

   #xAxisColId
   #xAxisOptionValueChanged

   #yAxisColIds
   #yAxisOptionValueChanged

   #yAxisRightColIds
   #colorAxisColId
   #sizeAxisColId

   #fitColIds
   #histoColIds

   #errorColIds

   #autoScaleOptionValue
   #autoScaleOptionValueChanged

   #fitNormalOptionValue
   #fitNormalOptionValueChanged

   #cumulativeOptionValue
   #cumulativeOptionValueChanged

   #normalizedOptionValue
   #normalizedOptionValueChanged

   #selection_row_visible

   #selection_x_visible
   #selection_y_visible
   #selection_color_visible
   #selection_size_visible
   #selection_y_multi_visible
   #selection_yr_multi_visible

   #yAxesPreselectIds

   #yAxisMultiSelectionIds
   #yAxisMultiFeatureMap
   #yAxisMultiOptionValuesChanged

   #yAxisRightMultiSelectionIds
   #yAxisRightMultiFeatureMap
   #yAxisRightMultiOptionValuesChanged

   #xAxisRejection
   #xAxisFeatureMap
   #xAxisOptionValuesChanged

   #yAxisRejection
   #yAxisFeatureMap
   #yAxisOptionValuesChanged

   #colorAxisRejection
   #colorAxisFeatureMap
   #colorAxisOptionValuesChanged

   #sizeAxisRejection
   #sizeAxisFeatureMap
   #sizeAxisOptionValuesChanged

   #fitColId
   #fitFeatureMap
   #fitOptionValuesChanged

   #histoColId
   #histoFeatureMap
   #histoOptionValuesChanged

   constructor(pars, ...args) {
      super(pars, ...args);

      this.#xAxisColId = pars?.xAxisColId ? pars.xAxisColId : "";
      this.#yAxisColIds = pars?.yAxisColId ? [pars.yAxisColId] : (pars?.yAxisColIds ? pars.yAxisColIds : []);
      this.#yAxisRightColIds = pars?.yAxisRightColIds ? pars.yAxisRightColIds : [];
      this.#colorAxisColId = pars?.colorAxisColId ? pars.colorAxisColId : "";
      this.#sizeAxisColId = pars?.sizeAxisColId ? pars.sizeAxisColId : "";
      this.#fitColIds = pars?.fitColIds ? pars.fitColIds : [];
      this.#fitColId = null;
      this.#histoColIds = pars?.histoColIds ? pars.histoColIds : [];
      this.#histoColId = null;
      this.#errorColIds = []

      this.#yAxesPreselectIds = pars?.yAxesColIdsDefaults ?? [];
      this.#yAxisMultiSelectionIds = this.#yAxesPreselectIds.length ? this.#yAxisColIds.filter(id =>  this.#yAxesPreselectIds.includes(id)) : this.#yAxisColIds;
      this.#yAxisRightMultiSelectionIds = this.#yAxesPreselectIds.length ? this.#yAxisRightColIds.filter(id =>  this.#yAxesPreselectIds.includes(id)) : this.#yAxisRightColIds;

      this.#selection_row_visible = pars?.tableRowSelectVisibility?.length ?? false;

      this.#selection_x_visible = pars?.selection_x_visible ?? false;
      this.#selection_y_visible = pars?.selection_y_visible ?? false;
      this.#selection_color_visible = pars?.selection_color_visible ?? false;
      this.#selection_size_visible = pars?.selection_size_visible ?? false;
      this.#selection_y_multi_visible = this.#yAxisMultiSelectionIds.length && (pars?.selection_y_multi_visible ?? false);
      this.#selection_yr_multi_visible = this.#yAxisRightMultiSelectionIds.length && (pars?.selection_yr_multi_visible ?? false);

      if(pars?.error_bars){
         let error_bars = pars.error_bars;
         for (const [key, value] of Object.entries(error_bars)) {
            if(this.#yAxisColIds.includes(key) || this.#yAxisRightColIds.includes(key))
               if(value.source_column)
                  this.#errorColIds.push(value.source_column)
          }
      }

      this.#xAxisRejection = pars?.axis_x_rejection ?? [];
      this.#xAxisOptionValuesChanged = new LimSignal([]);

      this.#yAxisRejection = pars?.axis_y_rejection ?? [];
      this.#yAxisOptionValuesChanged = new LimSignal([]);

      this.#colorAxisRejection = pars?.axis_color_rejection ?? [];
      this.#colorAxisOptionValuesChanged = new LimSignal([]);

      this.#sizeAxisRejection = pars?.axis_size_rejection ?? [];
      this.#sizeAxisOptionValuesChanged = new LimSignal([]);

      this.#yAxisMultiOptionValuesChanged = new LimSignal([]);
      this.#yAxisRightMultiOptionValuesChanged = new LimSignal([]);

      this.#fitOptionValuesChanged = new LimSignal([]);
      this.#histoOptionValuesChanged = new LimSignal([]);

      this.defaultGraphTools = ["box_select", "lasso_select", "hover", "pan", "reset", "save", "wheel_zoom"];

      this.optionList = new Map();
      if (this.#selection_row_visible) {
         this.optionList.set("selectedRowVisibility", {
            type: "selection",
            title: "Display Data",
         });
      }
      if (this.#selection_x_visible) {
         this.optionList.set("xAxis", {
            type: "selection",
            title: "X axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_x.svg"
         });
      }
      if (this.#selection_y_visible) {
         this.optionList.set("yAxis", {
            type: "selection",
            title: "Y axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_y.svg"
         });
      }
      if (this.#selection_y_multi_visible) {
         this.optionList.set("yAxisMulti", {
            type: "multi-selection",
            title: "Y axis features",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_y.svg"
         });
      }
      if (this.#selection_yr_multi_visible) {
         this.optionList.set("yAxisRightMulti", {
            type: "multi-selection",
            title: "Y axis right features",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_y_right.svg"
         });
      }
      if (this.#selection_color_visible) {
         this.optionList.set("colorAxis", {
            type: "selection",
            title: "Color axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_color.svg"
         });
      }
      if (this.#selection_size_visible) {
         this.optionList.set("sizeAxis", {
            type: "selection",
            title: "Size axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_size.svg"
         });
      }
      if (this.#fitColIds.length > 1) {
         this.optionList.set("fit", {
            type: "selection",
            title: "Fitted column",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/doseResponse_common.svg"
         });
      }
      if (this.#histoColIds.length > 1) {
         this.optionList.set("histo", {
            type: "selection",
            title: "X axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/histo_common.svg"
         });
      }

      if (pars?.fitNormalOptionVisible) {
         this.optionList.set("fitNormal", {
            type: "option",
            title: "Fit normal distribution",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/normalDistrib_common.svg"
         });
      }
      if (pars?.autoScaleOptionVisible) {
         this.optionList.set("autoScale", {
            type: "option",
            title: "Scale to fit the data",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/luts_autoscale.svg"
         });
      }
      if (pars?.cumulativeOptionVisible) {
         this.optionList.set("cumulative", {
            type: "option",
            title: "Cummulative histogram",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/histo_cumulative.svg"
         });
      }
      if (pars?.normalizedOptionVisible) {
         this.optionList.set("normalized", {
            type: "option",
            title: "Cummulative histogram",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/histo_relative.svg"
         });
      }

      this.#autoScaleOptionValue = !!pars?.autoScaleOptionValue;
      this.#autoScaleOptionValueChanged = new LimSignal(pars?.autoScaleOptionValueChanged ? [pars.autoScaleOptionValueChanged] : []);

      this.#fitNormalOptionValue = !!pars?.fitNormalOptionValue;
      this.#fitNormalOptionValueChanged = new LimSignal(pars?.fitNormalOptionValueChanged ? [pars.fitNormalOptionValueChanged] : []);

      this.#cumulativeOptionValue = !!pars?.cumulativeOptionValue;
      this.#cumulativeOptionValueChanged = new LimSignal(pars?.cumulativeOptionValueChanged ? [pars.cumulativeOptionValueChanged] : []);

      this.#normalizedOptionValue = !!pars?.normalizedOptionValue;
      this.#normalizedOptionValueChanged = new LimSignal(pars?.normalizedOptionValueChanged ? [pars.normalizedOptionValueChanged] : []);

      this.updateFeatureList();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get dataColumns() {
      let cols = this.tableData.systemColIdList;

      if (this.xAxisColId)
         cols.push(this.xAxisColId);
      if (this.yAxisColId)
         cols.push(this.yAxisColId);
      if (this.sizeAxisColId)
         cols.push(this.sizeAxisColId);
      if (this.colorAxisColId)
         cols.push(this.colorAxisColId);

      if (this.yAxisColIds.length)
         cols = cols.concat(this.yAxisColIds);
      if (this.yAxisRightColIds.length)
         cols = cols.concat(this.yAxisRightColIds);

      if (this.fitColId)
         cols.push(this.fitColId);
      if (this.histoColId)
         cols.push(this.histoColId);

      if(this.#errorColIds.length)
         cols = cols.concat(this.#errorColIds);

      return cols;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createOptionValues(table_data, rejection_list, axis_col_id = undefined) {
      if (!table_data)
         return new Map();

      const map = new Map();
      for (let i = 0; i < table_data.colCount; i++) {
         const meta = table_data.colMetadataAt(i);
         const col_id = table_data.colIdAt(i);
         if ((meta?.hidden ?? false) == false && !rejection_list.includes(col_id) || (axis_col_id && col_id == axis_col_id))
            map.set(col_id, table_data.colTitleAt(i));
      }
      return map;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   updateFeatureList() {
      this.#xAxisFeatureMap = new Map();
      this.#yAxisFeatureMap = new Map();
      this.#colorAxisFeatureMap = new Map();
      this.#sizeAxisFeatureMap = new Map();

      this.#yAxisMultiFeatureMap = new Map();
      this.#yAxisRightMultiFeatureMap = new Map();

      this.#fitFeatureMap = new Map();
      this.#histoFeatureMap = new Map();

      if (!this.tableData)
         return;

      if(this instanceof LimGraphScatterplotV2) {
         for (let i = 0; i < this.tableData.colCount; i++) {
            const meta = this.tableData.colMetadataAt(i);
            const col_id = this.tableData.colIdAt(i);
            if(meta.decltype == 'QString') {
               if (!this.#xAxisRejection.includes(col_id))
                  this.#xAxisRejection.push(col_id)
               if (!this.#yAxisRejection.includes(col_id))
                  this.#yAxisRejection.push(col_id)
               if (!this.#sizeAxisRejection.includes(col_id))
                  this.#sizeAxisRejection.push(col_id)
            }
         }
      }

      this.#xAxisFeatureMap = this.createOptionValues(this.tableData, this.#xAxisRejection, this.#xAxisColId);
      this.#xAxisOptionValuesChanged.emit(this);

      this.#yAxisFeatureMap = this.createOptionValues(this.tableData, this.#yAxisRejection, this.#yAxisColIds);
      this.#yAxisOptionValuesChanged.emit(this);

      this.#colorAxisFeatureMap = this.createOptionValues(this.tableData, this.#colorAxisRejection, this.#colorAxisColId);
      this.#colorAxisOptionValuesChanged.emit(this);

      this.#sizeAxisFeatureMap = this.createOptionValues(this.tableData, this.#sizeAxisRejection, this.#sizeAxisColId);
      this.#sizeAxisOptionValuesChanged.emit(this);

      for (let i = 0; i < this.#yAxisColIds.length; i++) {
         const col_id = this.#yAxisColIds[i];
         const index = this.tableData.colIndexById(col_id);
         if(index != -1)
            this.#yAxisMultiFeatureMap.set(col_id, this.tableData.colTitleAt(index));
      }
      this.#yAxisMultiOptionValuesChanged.emit(this);

      for (let i = 0; i < this.#yAxisRightColIds.length; i++) {
         const col_id = this.#yAxisRightColIds[i];
         const index = this.tableData.colIndexById(col_id);
         if(index != -1)
            this.#yAxisRightMultiFeatureMap.set(col_id, this.tableData.colTitleAt(index));
      }
      this.#yAxisRightMultiOptionValuesChanged.emit(this);

      for (let i = 0; i < this.#fitColIds.length; i++) {
         const col_id = this.#fitColIds[i];
         const index = this.tableData.colIndexById(col_id);
         if(index != -1)
            this.#fitFeatureMap.set(col_id, this.tableData.colTitleAt(index));
      }
      this.#fitOptionValuesChanged.emit(this);

      for (let i = 0; i < this.#histoColIds.length; i++) {
         const col_id = this.#histoColIds[i];
         const index = this.tableData.colIndexById(col_id);
         if(index != -1)
            this.#histoFeatureMap.set(col_id, this.tableData.colTitleAt(index));
      }
      this.#histoOptionValuesChanged.emit(this);
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async createCustomToolBar(fig) {

      let pans = [];
      let toolbar_tools = fig.toolbar.tools;
      for (let i = 0; i < toolbar_tools.length; i++) {
         if (toolbar_tools[i].tool_name == "Pan")
            pans.push(toolbar_tools[i].dimensions);
      }
      for (let i = 0; i < toolbar_tools.length; i++) {
         if (toolbar_tools[i].tool_name == "Pan") {
            if (toolbar_tools[i].dimensions == 'both')
               toolbar_tools[i].active = true;
            else
               toolbar_tools[i].active = !pans.includes('both');
         }
      }

      const createCanvas = () => {
         let container = this.containerGraph
         let figure_canvas = [];
         if (!container)
            return;
         let figures = container.getElementsByClassName('bk-Figure');
         if (!figures.length) {
            let rows = container.getElementsByClassName('bk-Row');
            if (rows.length)
               figures = rows[0].shadowRoot.children;
         }
         for (let i = 0; i < figures.length; i++) {
            if (figures[i].classList.contains('bk-Figure')) {
               let fig = figures[i];
               let div_canvas = fig.shadowRoot?.children ?? [];
               for (let i = 0; i < div_canvas.length; i++) {
                  if (div_canvas[i].classList.contains('bk-Canvas')) {
                     let canvas = div_canvas[i].shadowRoot?.children ?? [];
                     for (let i = 0; i < canvas.length; i++) {
                        if (canvas[i] instanceof HTMLCanvasElement) {
                           figure_canvas.push(canvas[i]);
                           break;
                        }
                     }
                  }
               }
            }
         }

         let zoom = window.outerWidth / window.innerWidth;

         if (figure_canvas.length) {
            let xmin = Number.POSITIVE_INFINITY;
            let xmax = Number.NEGATIVE_INFINITY;
            let ymin = Number.POSITIVE_INFINITY;
            let ymax = Number.NEGATIVE_INFINITY;
            for (let i = 0; i < figure_canvas.length; i++) {
               let rect = figure_canvas[i].getBoundingClientRect();
               xmin = Math.min(xmin, rect.left * zoom);
               xmax = Math.max(xmax, rect.right * zoom);
               ymin = Math.min(ymin, rect.top * zoom);
               ymax = Math.max(ymax, rect.bottom * zoom);
            }
            let final_canvas = document.createElement('canvas');
            let ctx = final_canvas.getContext('2d');
            final_canvas.width = xmax - xmin;
            final_canvas.height = ymax - ymin;
            ctx.fillStyle = fig.background_fill_color;
            ctx.fillRect(0, 0, final_canvas.width, final_canvas.height);

            for (let i = 0; i < figure_canvas.length; i++) {
               let rect = figure_canvas[i].getBoundingClientRect();
               ctx.drawImage(figure_canvas[i], rect.left * zoom - xmin, rect.top * zoom - ymin, rect.width * zoom, rect.height * zoom);
            }
            if (final_canvas.width > 0)
               return final_canvas;
         }
      };


      const buttonIsPressed = function() {
          return this.ariaPressed === "true";
      }

      const buttonTogglePressed = function() {
         const press = !(this.ariaPressed === "true");
         this.ariaPressed = press ? "true" : "false";
         return press;
      }

      const createToolButton = (title, svgname, pressed, onclick) => {
         const button = document.createElement("button");
         button.title = title;
         button.ariaPressed = pressed ? "true" : "false";
         const img = document.createElement("img");
         img.src = `/res/gnr_core_gui/CoreGUI/Icons/base/${svgname}.svg`;
         button.appendChild(img);
         button.onclick = onclick ? onclick.bind(button) : undefined;
         button.isPressed = buttonIsPressed.bind(button);
         button.togglePressed = buttonTogglePressed.bind(button);
         return button;
      }

      const button_pan = createToolButton("Pan", "pan", true, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Pan" && toolbar.tools[i].dimensions == "both")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_xpan = createToolButton("Horizontal Pan", "x-pan", !pans.includes('both'), function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Pan" && toolbar.tools[i].dimensions == "width")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_lasso = createToolButton("Lasso Select", "lasso-select", false, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Lasso Select")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_box = createToolButton("Box Select", "box-select", false, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Box Select")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_zoom = createToolButton("Wheel Zoom", "wheel-zoom", true, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Wheel Zoom")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_xzoom = createToolButton("Wheel Zoom", "wheel-zoom", true, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Wheel Zoom")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_reset = createToolButton("Reset", "refresh", false, function() {
         fig.reset.emit();
      });

      const button_export = createToolButton("Export", "save", false, function() {
         let canvas = createCanvas();
         if (!canvas)
            return;
         const dataURL = canvas.toDataURL('image/png');
         let download = document.createElement('a');
         download.href = dataURL;
         download.download = 'graph';
         download.click();
      });

      const button_clipboard = createToolButton("Copy to Clipboard", "clipboard", false, function() {
         let canvas = createCanvas();
         if (!canvas)
            return;
         canvas.toBlob(function (blob) {
            const item = new ClipboardItem({ "image/png": blob });
            navigator.clipboard.write([item]);
         });
      });

      const button_hover = createToolButton("Hover", "hover", false, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Hover")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_tap = createToolButton("Tap", "tap", false, function() {
         const pressed = this.togglePressed();
         let toolbar = fig.toolbar;
         for (let i = 0; i < toolbar.tools.length; i++)
            if (toolbar.tools[i].tool_name == "Tap")
               if (toolbar.tools[i].active != pressed)
                  toolbar.tools[i].active = pressed;
      });

      const button_frame = createToolButton("Select Frame", "select_frame", false);
      const frame_selection_enabled = this.graphToolsAll.includes("frame_select")
         && this.createFrameSpan(fig, button_frame, [button_pan, button_xpan, button_lasso, button_box]);

      let fnButtons = (that, button, grouped_buttons) => {
         if (button.isPressed() != that.active)
            button.togglePressed();
         if (that.active)
            grouped_buttons.forEach((btn) => {
               if (btn.isPpressed())
                  btn.onclick();
            });
      }

      let tools = fig.toolbar.tools;
      for (let i = 0; i < tools.length; i++)
         switch (tools[i].tool_name) {
            case "Wheel Zoom": {
               tools[i].maintain_focus = false;
            }
               break;
            case "Pan":
               switch (tools[i].dimensions) {
                  case "both":
                     tools[i].js_property_callbacks = {
                        "change:active": [new Bokeh.CustomJS({
                           args: { button: button_pan, grouped_buttons: [button_frame], fn: fnButtons },
                           code: "fn(cb_obj, button, grouped_buttons)"
                        })]
                     }; break;
                  case "width":
                     tools[i].js_property_callbacks = {
                        "change:active": [new Bokeh.CustomJS({
                           args: { button: button_xpan, grouped_buttons: [button_frame], fn: fnButtons },
                           code: "fn(cb_obj, button, grouped_buttons)"
                        })]
                     }; break;
               }
               break;
            case "Lasso Select":
               tools[i].js_property_callbacks = {
                  "change:active": [new Bokeh.CustomJS({
                     args: { button: button_lasso, grouped_buttons: [button_frame], fn: fnButtons },
                     code: "fn(cb_obj, button, grouped_buttons)"
                  })]
               };
               tools[i].continuous = false;
               break;
            case "Box Select":
               tools[i].js_property_callbacks = {
                  "change:active": [new Bokeh.CustomJS({
                     args: { button: button_box, grouped_buttons: [button_frame], fn: fnButtons },
                     code: "fn(cb_obj, button, grouped_buttons)"
                  })]
               };
               tools[i].continuous = false;
               break;
            case "Tap":
               tools[i].js_property_callbacks = {
                  "change:active": [new Bokeh.CustomJS({
                     args: { button: button_tap, fn: fnButtons },
                     code: "fn(cb_obj, button, [])"
                  })]
               };
               break;
         }

      fig.toolbar_location = null;
      this.containerToolbar.innerHTML = ""

      if (this.graphToolsAll.includes("pan"))
         this.containerToolbar.appendChild(button_pan);
      if (this.graphToolsAll.includes("xpan"))
         this.containerToolbar.appendChild(button_xpan);
      if (this.graphToolsAll.includes("lasso_select"))
         this.containerToolbar.appendChild(button_lasso);
      if (this.graphToolsAll.includes("box_select"))
         this.containerToolbar.appendChild(button_box);
      if(this.graphToolsAll.includes("frame_select") && frame_selection_enabled )
         this.containerToolbar.appendChild(button_frame);
      if (this.graphToolsAll.includes("wheel_zoom"))
         this.containerToolbar.appendChild(button_zoom);
      if (this.graphToolsAll.includes("xwheel_zoom"))
         this.containerToolbar.appendChild(button_xzoom);
      if (this.graphToolsAll.includes("tap"))
         this.containerToolbar.appendChild(button_tap);
      if (this.graphToolsAll.includes("hover"))
         this.containerToolbar.appendChild(button_hover);
      if (this.graphToolsAll.includes("reset"))
         this.containerToolbar.appendChild(button_reset);
      if (this.graphToolsAll.includes("save"))
         this.containerToolbar.appendChild(button_export);
      if (this.graphToolsAll.includes("clipboard"))
         this.containerToolbar.appendChild(button_clipboard);
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get xAxisColId() {
      return this.#xAxisColId;
   }
   get yAxisColIds() {
      return this.#yAxisColIds;
   }
   get yAxisRightColIds() {
      return this.#yAxisRightColIds;
   }
   get colorAxisColId() {
      return this.#colorAxisColId;
   }
   get sizeAxisColId() {
      return this.#sizeAxisColId;
   }
   get fitColId() {
      return this.#fitColId ?? (this.#fitColIds.length ? this.#fitColIds[0] : '');
   }
   get histoColId() {
      return this.#histoColId ?? (this.#histoColIds.length ? this.#histoColIds[0] : '');
   }

   set xAxisColId(val) {
      if (this.#xAxisColId === val)
         return;
      this.#xAxisColId = val;
      this.#xAxisOptionValueChanged.emit(this);
      this.update();
   }
   set yAxisColIds(val) {
      if (this.#yAxisColIds === val)
         return;
      this.#yAxisColIds = val;
      this.#yAxisOptionValueChanged.emit(this);
      this.update();
   }

   // axis X selection
   get xAxisOptionValue() {
      return this.#xAxisColId;
   }
   set xAxisOptionValue(val) {
      if (this.#xAxisColId === val)
         return;
      this.#xAxisColId = val;
      this.#xAxisOptionValuesChanged.emit(this);
      this.update();
   }
   get xAxisOptionValues() {
      return [...this.#xAxisFeatureMap.entries()];
   }

   // axis Y selection
   get yAxisOptionValue() {
      return this.#yAxisColIds[0];
   }
   set yAxisOptionValue(val) {
      if (this.#yAxisColIds[0] === val)
         return;
      this.#yAxisColIds = [val];
      this.#yAxisOptionValuesChanged.emit(this);
      this.update();
   }
   get yAxisOptionValues() {
      return [...this.#yAxisFeatureMap.entries()];
   }

   // axis COLOR selection
   get colorAxisOptionValue() {
      return this.#colorAxisColId === "" ? "-" : this.#colorAxisColId;
   }
   set colorAxisOptionValue(val) {
      if (this.#colorAxisColId === val)
         return;
      this.#colorAxisColId = val;
      this.#colorAxisOptionValuesChanged.emit(this);
      this.update();
   }
   get colorAxisOptionValues() {
      return [["-", "&lt;none&gt;"], ...this.#colorAxisFeatureMap.entries()];
   }

   // axis SIZE selection
   get sizeAxisOptionValue() {
      return this.#sizeAxisColId === "" ? "-" : this.#sizeAxisColId;
   }
   set sizeAxisOptionValue(val) {
      if (this.#sizeAxisColId === val)
         return;
      this.#sizeAxisColId = val;
      this.#sizeAxisOptionValuesChanged.emit(this);
      this.update();
   }
   get sizeAxisOptionValues() {
      return [["-", "&lt;none&gt;"], ...this.#sizeAxisFeatureMap.entries()];
   }

   // axis Y LEFT multi-selection
   get yAxisMultiOptionValue() {
      return this.#yAxisMultiSelectionIds;
   }
   set yAxisMultiOptionValue(val) {
      if (this.#yAxisMultiSelectionIds === val)
         return;
      this.#yAxisMultiSelectionIds = val;
      this.#yAxisMultiOptionValuesChanged.emit(this);
      this.update();
   }
   get yAxisMultiOptionValues() {
      return [...this.#yAxisMultiFeatureMap.entries()];
   }

   // axis Y RIGHT multi-selection
   get yAxisRightMultiOptionValue() {
      return this.#yAxisRightMultiSelectionIds;
   }
   set yAxisRightMultiOptionValue(val) {
      if (this.#yAxisRightMultiSelectionIds === val)
         return;
      this.#yAxisRightMultiSelectionIds = val;
      this.#yAxisRightMultiOptionValuesChanged.emit(this);
      this.update();
   }
   get yAxisRightMultiOptionValues() {
      return [...this.#yAxisRightMultiFeatureMap.entries()];
   }

   // FIT selection
   get fitOptionValue() {
      return this.fitColId;
   }
   set fitOptionValue(val) {
      if (this.#fitColId && this.#fitColId === val)
         return;
      this.#fitColId = val;
      this.#fitOptionValuesChanged.emit(this);
      this.update();
   }
   get fitOptionValues() {
      return [...this.#fitFeatureMap.entries()];
   }

   // HISTO selection
   get histoOptionValue() {
      return this.histoColId;
   }
   set histoOptionValue(val) {
      if (this.#histoColId && this.#histoColId === val)
         return;
      this.#histoColId = val;
      this.#histoOptionValuesChanged.emit(this);
      this.update();
   }
   get histoOptionValues() {
      return [...this.#histoFeatureMap.entries()];
   }

   // selection signals
   get xAxisOptionValuesChanged() { return this.#xAxisOptionValuesChanged; }
   get yAxisOptionValuesChanged() { return this.#yAxisOptionValuesChanged; }
   get colorAxisOptionValuesChanged() { return this.#colorAxisOptionValuesChanged; }
   get sizeAxisOptionValuesChanged() { return this.#sizeAxisOptionValuesChanged; }
   get yAxisMultiOptionValuesChanged() { return this.#yAxisMultiOptionValuesChanged; }
   get yAxisRightMultiOptionValuesChanged() { return this.#yAxisRightMultiOptionValuesChanged; }
   get fitOptionValuesChanged() { return this.#fitOptionValuesChanged; }
   get histoOptionValuesChanged() { return this.#histoOptionValuesChanged; }

   // selections visibility
   get selection_x_visible() { return this.#selection_x_visible; }
   get selection_y_visible() { return this.#selection_y_visible; }
   get selection_color_visible() { return this.#selection_color_visible; }
   get selection_size_visible() { return this.#selection_size_visible; }
   get selection_y_multi_visible() { return this.#selection_y_multi_visible; }
   get selection_yr_multi_visible() { return this.#selection_yr_multi_visible; }

   // autoscale
   get autoScaleOptionValue() {
      return this.#autoScaleOptionValue;
   }
   set autoScaleOptionValue(val) {
      if (this.#autoScaleOptionValue === val)
         return;
      this.#autoScaleOptionValue = val;
      this.#autoScaleOptionValueChanged.emit(this);
      this.update();
   }
   get autoScaleOptionValueChanged() {
      return this.#autoScaleOptionValueChanged
   }

   // fit gaussian
   get fitNormalOptionValue() {
      return this.#fitNormalOptionValue;
   }
   set fitNormalOptionValue(val) {
      if (this.#fitNormalOptionValue === val)
         return;
      this.#fitNormalOptionValue = val;
      this.#fitNormalOptionValueChanged.emit(this);
      this.update();
   }
   get fitNormalOptionValueChanged() {
      return this.#fitNormalOptionValueChanged
   }

   // cumulative
   get cumulativeOptionValue() {
      return this.#cumulativeOptionValue;
   }
   set cumulativeOptionValue(val) {
      if (this.#cumulativeOptionValue === val)
         return;
      this.#cumulativeOptionValue = val;
      this.#cumulativeOptionValueChanged.emit(this);
      this.update();
   }
   get cumulativeOptionValueChanged() {
      return this.#cumulativeOptionValueChanged
   }

   // normalized
   get normalizedOptionValue() {
      return this.#normalizedOptionValue;
   }
   set normalizedOptionValue(val) {
      if (this.#normalizedOptionValue === val)
         return;
      this.#normalizedOptionValue = val;
      this.#normalizedOptionValueChanged.emit(this);
      this.update();
   }
   get normalizedOptionValueChanged() {
      return this.#normalizedOptionValueChanged
   }
}

/*___________________________________________________________________________*/
class DataSource {
   #x_range_name
   #y_range_name

   #data_scatter
   #data_lines
   #data_bands
   #data_error_bars

   #renderer_scatter
   #renderers_lines
   #renderers_scatter_legend
   #annotation_bands
   #annotation_labels

   #data_legend
   #color_mapper
   #size_bar_properties

   #color_map_title

   #range_x_min
   #range_x_max
   #range_y_min
   #range_y_max

   #ordered
   #default_visibility_line
   #default_visibility_marker

   #selectionChanged

   constructor(x_range_name, y_range_name) {
      this.#x_range_name = x_range_name;
      this.#y_range_name = y_range_name;

      this.#ordered = false;
      this.#default_visibility_line = true;
      this.#default_visibility_marker = true;

      this.#selectionChanged = new LimSignal([]);
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createData(pars) {
      const table_data = pars?.table_data;
      const col_ids = pars?.col_ids;
      const colorizer = pars?.colorizer;
      const series_style = pars?.series_style;
      const error_bars = pars?.error_bars;
      let default_label_type = pars?.default_label_type;
      const col_ids_visible = pars?.col_ids_visible;
      const autoscale = pars?.autoscale;

      this.#data_scatter = new Bokeh.ColumnDataSource();
      this.#data_lines = [];
      this.#data_bands = [];
      this.#data_legend = [];
      this.#data_error_bars = [];
      this.#color_mapper = null;
      this.#size_bar_properties = null;

      this.#color_map_title = "";

      const x_col_id = col_ids?.x ?? "";
      let y_col_ids = col_ids?.y ?? [];
      const color_col_id = col_ids?.color ?? [];
      const size_col_id = col_ids?.size ?? [];

      if (!table_data)
         return;

      const xColIndex = table_data.colIndexById(x_col_id);
      const yColIndexes = y_col_ids.map(id => table_data.colIndexById(id)).filter(index => index !== -1);
      if (xColIndex < 0 || !yColIndexes.length)
         return;
      y_col_ids = y_col_ids.filter(id => table_data.colIndexById(id) !== -1);

      let x = table_data.colDataAt(xColIndex);
      if (!x)
         return;
      let x_title = table_data.colMetadataAt(xColIndex).title ?? "";
      let x_units = table_data.colMetadataAt(xColIndex).units ?? "";

      let colors = [];
      if (color_col_id) {
         let colorColIndex = table_data.colIndexById(color_col_id);
         if (colorColIndex != -1) {
            let palette256 = colorizer?.color_axis_palette256() ?? [];
            const z_decltype = table_data.colMetadataAt(colorColIndex).decltype;
            if (z_decltype === "double" || z_decltype === "int") {
               const color_data = table_data.colDataAt(colorColIndex);
               let color_min = Number.POSITIVE_INFINITY;
               let color_max = Number.NEGATIVE_INFINITY;
               for (let i = 0; i < color_data.length; i++) {
                  color_min = Math.min(color_min, color_data[i]);
                  color_max = Math.max(color_max, color_data[i]);
               }
               if (!autoscale) {
                  const color_meta = table_data.colMetadataAt(colorColIndex);
                  const loc_min = color_meta.globalRange?.min ?? color_min;
                  const loc_max = color_meta.globalRange?.max ?? color_max;
                  color_min = Math.min(color_min, loc_min);
                  color_max = Math.max(color_max, loc_max);
               }
               const rng = color_max - color_min;
               if (Number.isInteger(palette256[0]))
                  for (let i = 0; i < color_data.length; i++)
                     colors.push(integerToRGB(palette256[Math.round(255 * ((color_data[i] - color_min) / rng))]));
               else
                  for (let i = 0; i < color_data.length; i++)
                     colors.push(palette256[Math.round(255 * ((color_data[i] - color_min) / rng))]);
               this.#color_mapper = colorizer.color_axis_is_linear
                  ? new Bokeh.LinearColorMapper({ low: color_min, high: color_max, palette: palette256 })
                  : new Bokeh.LogColorMapper({ low: color_min, high: color_max, palette: palette256 });
               this.#color_map_title = !colorizer.color_axis_title_visible ? '' : colorizer.color_axis_title_value.length ? colorizer.color_axis_title_value : table_data.colMetadata(color_col_id).title;
            }
            else {
               const color_data = table_data.colDataAt(colorColIndex);
               const labels = [... new Set(color_data)];
               const specific_colors = colorizer.specific_colors();
               let paletteN = Bokeh.Palettes.linear_palette(palette256, labels.length);
               for(let i=0; i<labels.length; i++) {
                  const label = labels[i];
                  if(label in specific_colors)
                     paletteN[i] = specific_colors[label];
               }
               for (const val of color_data)
                  colors.push(integerToRGB(paletteN[labels.indexOf(val)]))
               this.#color_mapper = new Bokeh.CategoricalColorMapper({ factors: labels, palette: paletteN });
               this.#color_map_title = !colorizer.color_axis_title_visible ? '' : colorizer.color_axis_title_value.length ? colorizer.color_axis_title_value : table_data.colMetadata(color_col_id).title;
            }
         }
      }
      let sizes = [];
      if (size_col_id) {
         let sizeColIndex = table_data.colIndexById(size_col_id);
         if (sizeColIndex != -1) {
            if (true) {
               //if (zmeta.decltype === "double" || zmeta.decltype === "int") {
               const size_data = table_data.colDataAt(sizeColIndex);
               let size_min = Number.POSITIVE_INFINITY;
               let size_max = Number.NEGATIVE_INFINITY;
               for (let i = 0; i < size_data.length; i++) {
                  size_min = Math.min(size_min, size_data[i]);
                  size_max = Math.max(size_max, size_data[i]);
               }
               if (!autoscale) {
                  const size_meta = table_data.colMetadataAt(sizeColIndex);
                  const loc_min = size_meta.globalRange?.min ?? size_min;
                  const loc_max = size_meta.globalRange?.max ?? size_max;
                  size_min = Math.min(size_min, loc_min);
                  size_max = Math.max(size_max, loc_max);
               }
               const rng = size_max - size_min;
               if (rng > 0)
                  sizes = size_data.map(size => 5.0 + 25.0 * (size - size_min) / rng);
               else
                  sizes = new Array(size_data.length).fill(10);

               this.#size_bar_properties = {
                  title: table_data.colMetadata(size_col_id).title,
                  min: size_min,
                  max: size_max,
                  colored: (size_col_id === color_col_id) && colorizer.color_axis_is_linear
               }
            }
            // else {
            //    const labels = [... new Set(this.#graphDataSource.data.z)];
            //    for (const val of this.#graphDataSource.data.z) {
            //       colors.push(interpolator(Math.max(0, labels.indexOf(val)) / (labels.length - 1)));
            //    }

            //    color_mapper = new Bokeh.CategoricalColorMapper({ palette: colors, factors: labels });
            // }
         }
      }

      let groupedBy = table_data.groupedBy;
      const is_grouped = groupedBy.length > 0;
      let groupedBy_titles = groupedBy.map(id => table_data.colTitle(id));
      let groupedBy_values = [];
      let grouping = [];
      let group_begins = [];
      if (is_grouped) {
         let rows = 0;
         const groups = table_data.groups;
         for (let g = 0; g < groups.length; g++) {
            group_begins.push(rows);
            groupedBy_values.push(new Array(...groupedBy.map(colId => table_data.colData(colId, [rows]))));
            rows += groups[g].length;
            for (let i = 0; i < groups[g].length; i++)
               grouping.push(g);
         }
      }
      else {
         group_begins.push(0);
         grouping = new Array(x.length).fill(0);
      }

      let sort_order = [...x.keys()];
      if (this.#ordered) {
         sort_order.sort((a, b) => {
            if (grouping[a] != grouping[b])
               return grouping[a] - grouping[b];
            else
               return x[a] - x[b];
         });
         x = sort_order.map(i => x[i]);
         if (colors.length)
            colors = sort_order.map(i => colors[i]);
         if (sizes.length)
            sizes = sort_order.map(i => colors[i]);
      }

      let group_names = [];
      for (let i = 0; i < groupedBy_values.length; i++) {
         let name = '';
         for (let j = 0; j < groupedBy_values[i].length; j++)
            name += (j > 0 ? ', ' : '') + groupedBy_titles[j] + ': ' + groupedBy_values[i][j];
         group_names.push(name);
      }

      let group_count = group_begins.length;
      let column_length = x.length;

      let skipped = 0;
      for (let i = 0; i < y_col_ids.length; i++) {
         const y_col_id = y_col_ids[i];
         if (!col_ids_visible.includes(y_col_id)) {
            skipped++;
            continue;
         }

         this.#data_legend.push(...group_begins.map((row, index) => ({
            label: table_data.colMetadata(y_col_ids[i]).title + (is_grouped ? (' [' + group_names[index] + ']') : ''),
            index: (i - skipped) * column_length + row
         })));
      }

      let ident = table_data.rowTitlesLong;
      if (this.#ordered)
          ident = sort_order.map(i => ident[i]);

      let data = {
         x: [], y: [], y_column_id: [], row_id: [],
         fill_color: [], fill_alpha: [], marker: [], size: [],
         label: [], group: [], ident: [],
         x_title: [], x_units: [],
         y_title: [], y_units: []
      };
      if (size_col_id && table_data.colIndexById(size_col_id) != -1) {
         data.size_value = [];
         data.size_title = [];
         data.size_units = [];
      }
      if (color_col_id && table_data.colIndexById(color_col_id) != -1) {
         data.color_value = [];
         data.color_title = [];
         data.color_units = [];
      }

      skipped = 0;
      for (let i = 0; i < yColIndexes.length; i++) {
         const y_col_id = y_col_ids[i];
         if (!col_ids_visible.includes(y_col_id)) {
            skipped++;
            continue;
         }

         const y_col_index = yColIndexes[i];
         let y_title = table_data.colMetadata(y_col_id).title ?? "";
         let y_units = table_data.colMetadata(y_col_id).units ?? "";

         let y = table_data.colDataAt(y_col_index);
         if (this.#ordered)
            y = sort_order.map(i => y[i]);
         for (let j = 0; j < x.length; j++) {
            // if(x[j] === null || y[j] === null)
            //    continue;
            data.x.push(x[j]);
            data.y.push(y[j] !== null ? y[j] : NaN);
            data.y_column_id.push(y_col_id);
            data.group.push(y_title + '[' + grouping[j].toString() + ']');
            data.ident.push(ident[j]);
         }

         if (!this.#ordered)
            for (let j = 0; j < x.length; j++)
               data.row_id.push(j);
         else
            for (let j = 0; j < x.length; j++)
               data.row_id.push(sort_order[j]);

         for (let j = 0; j < x.length; j++) {
            data.x_title.push(x_title);
            data.x_units.push(x_units);
            data.y_title.push(y_title);
            data.y_units.push(y_units);
         }

         //values for tooltip
         if (size_col_id) {
            const col_index = table_data.colIndexById(size_col_id);
            if (col_index != -1) {
               let values = table_data.colDataAt(col_index);
               let title = table_data.colMetadataAt(col_index).title ?? "";
               let units = table_data.colMetadataAt(col_index).units ?? "";
               if (this.#ordered)
                  values = sort_order.map(i => values[i]);
               for (let j = 0; j < x.length; j++) {
                  data.size_value.push(values[j]);
                  data.size_title.push(title);
                  data.size_units.push(units);
               }
            }
         }
         if (color_col_id) {
            const col_index = table_data.colIndexById(color_col_id);
            if (col_index != -1) {
               let values = table_data.colDataAt(col_index);
               let title = table_data.colMetadataAt(col_index).title ?? "";
               let units = table_data.colMetadataAt(col_index).units ?? "";
               if (this.#ordered)
                  values = sort_order.map(i => values[i]);
               for (let j = 0; j < x.length; j++) {
                  data.color_value.push(values[j]);
                  data.color_title.push(title);
                  data.color_units.push(units);
               }
            }
         }

         //color
         let style = series_style.at(y_col_id);
         if (colors.length) {
            for (let j = 0; j < x.length; j++) {
               data.fill_color.push(colors[j]);
               data.fill_alpha.push(style.opacity);
            }
         }
         else if (style.color) {
            //const colorRGB = integerToRGB(style.color);
            for (let j = 0; j < x.length; j++) {
               data.fill_color.push(style.color);
               data.fill_alpha.push(style.opacity);
            }
         }
         else {
            for (let j = 0; j < x.length; j++) {
               data.fill_color.push(colorizer.color(i, grouping[j], group_count));
               data.fill_alpha.push(style.opacity);
            }
         }

         //marker
         if (style.marker_type && style.marker_type != "none")
            for (let j = 0; j < x.length; j++) {
               data.marker.push(style.marker_type);
               data.size.push(sizes.length ? sizes[j] : style.marker_size);
            }
         else if (this.#default_visibility_marker)
            for (let j = 0; j < x.length; j++) {
               data.marker.push(colorizer.marker(i, grouping[j], group_count));
               data.size.push(sizes.length ? sizes[j] : style.marker_size);
            }
         else
            for (let j = 0; j < x.length; j++) {
               data.marker.push(null);
               data.size.push(0);
            }

         //labels
         let labels = [];
         default_label_type = style.label_type ? style.label_type : default_label_type;
         switch (default_label_type) {
            case 'value_x':
               labels = x.map(x => x.toLocaleString({ maximumFractionDigits: 3 }));
               break;
            case 'value_y':
               labels = y.map(y => y.toLocaleString({ maximumFractionDigits: 3 }));
               break;
            case 'value_xy':
               labels = x.map((x, index) => '[' + x.toLocaleString({ maximumFractionDigits: 3 }) + '; ' + y[index].toLocaleString({ maximumFractionDigits: 3 }) + ']');
               break;
            case 'series_first':
               labels = new Array(x.length).fill('');
               for (let i = 0; i < group_begins.length; i++)
                  labels[group_begins[i]] = group_names[i];
               break;
            case 'series_last':
               labels = new Array(x.length).fill('');
               for (let i = 1; i < group_begins.length; i++)
                  labels[group_begins[i] - 1] = group_names[i - 1];
               labels[labels.length - 1] = group_names[group_begins.length - 1];
               break;
            case 'none':
            default:
               labels = new Array(x.length).fill('');
         }

         for (let j = 0; j < x.length; j++)
            data.label.push(labels[j]);

         //=== lines
         if ((style.line_dash || this.#default_visibility_line) && style.line_dash != "none") {
            const start = (i - skipped) * column_length;
            for (let g = 0; g < group_count; g++) {
               const rows = grouping.map(ind => ind === g).reduce((out, bool, index) => bool ? out.concat(index) : out, []).map(row => row + start);
               const color_default = colorizer.color(i, g, group_count);
               if (!rows.length)
                  continue;
               const line_color = style.line_color ? style.line_color : color_default;
               this.#data_lines.push({
                  index: rows[0],
                  scatter_indices: rows,
                  line_dash: style.line_dash ? style.line_dash : "solid",
                  line_width: style.line_width,
                  line_color: line_color,
                  line_alpha: style.line_opacity,
                  data_source: new Bokeh.ColumnDataSource({
                     data: {
                        x: rows.map(i => data.x[i]),
                        y: rows.map(i => data.y[i]),
                        x_title: new Array(rows.length).fill(x_title),
                        y_title: new Array(rows.length).fill(y_title),
                        x_units: new Array(rows.length).fill(x_units),
                        y_units: new Array(rows.length).fill(y_units),
                        line_color: new Array(rows.length).fill(line_color)
                     }
                  })
               });
               if (group_names.length)
                  this.#data_lines[this.#data_lines.length - 1].data_source.data.group_title = new Array(rows.length).fill(group_names[g]);
            }
         }
         //=== under fill
         if (style.fill_opacity > 0) {
            const start = (i - skipped) * column_length;
            for (let g = 0; g < group_count; g++) {
               const rows = grouping.map(ind => ind === g).reduce((out, bool, index) => bool ? out.concat(index) : out, []).map(row => row + start);
               const color_default = colorizer.color(i, g, group_count);
               if (!rows.length)
                  continue;
               let band_x = rows.map(i => data.x[i]);
               let band_y = rows.map(i => data.y[i]);
               for (let k = 0; k < band_x.length - 1; k++) {
                  if (!Number.isNaN(band_y[k]) && Number.isNaN(band_y[k + 1])) {
                     if (band_y[k] == 0)
                        continue;
                     band_x.splice(k + 1, 0, band_x[k]);
                     band_y.splice(k + 1, 0, 0.0);
                  }
                  if (Number.isNaN(band_y[k]) && !Number.isNaN(band_y[k + 1])) {
                     if (band_y[k + 1] == 0)
                        continue;
                     band_x.splice(k + 1, 0, band_x[k + 1]);
                     band_y.splice(k + 1, 0, 0.0);
                  }
               }
               this.#data_bands.push({
                  index: rows[0],
                  scatter_indices: rows,
                  fill_color: style.fill_color ? style.fill_color : color_default,//data.fill_color[rows[0]],
                  fill_opacity: style.fill_opacity,
                  data_source: new Bokeh.ColumnDataSource({ data: { x: band_x, y: band_y } })
               });
            }
         }
         //=== error_bars
         const error_bar = error_bars.at(y_col_id);
         if (error_bar.source_column_id && table_data.colIndexById(error_bar.source_column_id) != -1) {
            const error_col_id = table_data.colIndexById(error_bar.source_column_id);
            let diff = table_data.colDataAt(error_col_id);
            const start = (i - skipped) * column_length;
            for (let g = 0; g < group_count; g++) {
               const rows = grouping.map(ind => ind === g).reduce((out, bool, index) => bool ? out.concat(index) : out, []).map(row => row + start);
               if (!rows.length)
                  continue;
               this.#data_error_bars.push({
                  index: rows[0],
                  line_color: error_bar.line_color ? error_bar.line_color : data.fill_color[rows[0]],
                  line_width: error_bar.line_width,
                  whisker_line_width: error_bar.whisker_line_width,
                  data_source: new Bokeh.ColumnDataSource({ data: { base: rows.map(i => data.x[i]), lower: rows.map(i => data.y[i] - Number(diff[i - start])), upper: rows.map(i => data.y[i] + Number(diff[i - start])) } })
               });
            }
         }
      }
      this.#data_scatter.data = data;

      this.#range_x_min = Number.POSITIVE_INFINITY;
      this.#range_x_max = Number.NEGATIVE_INFINITY;
      this.#range_y_min = Number.POSITIVE_INFINITY;
      this.#range_y_max = Number.NEGATIVE_INFINITY;
      for (let i = 0; i < x.length; i++) {
         const value_x = x[i];
         if (Number.isFinite(value_x)) {
            this.#range_x_min = Math.min(this.#range_x_min, value_x);
            this.#range_x_max = Math.max(this.#range_x_max, value_x);
         }
      }
      const y_data = this.#data_scatter.data['y'];
      for (let i = 0; i < y_data.length; i++) {
         const value_y = y_data[i];
         if (Number.isFinite(value_y)) {
            this.#range_y_min = Math.min(this.#range_y_min, value_y);
            this.#range_y_max = Math.max(this.#range_y_max, value_y);
         }
      }

      for (let i = 0; i < this.#data_error_bars.length; i++) {
         const lower = this.#data_error_bars[i].data_source.data.lower;
         const upper = this.#data_error_bars[i].data_source.data.upper;
         for (let j = 0; j < lower.length; j++) {
            if (Number.isFinite(lower[j]))
               this.#range_y_min = Math.min(this.#range_y_min, lower[j]);
            if (Number.isFinite(upper[j]))
               this.#range_y_max = Math.max(this.#range_y_max, upper[j]);
         }
      }

      colorizer.increaseShift(yColIndexes.length);

      this.#data_scatter.selected.js_property_callbacks = {
         "change:indices": [new Bokeh.CustomJS({
            args: {
               fn: (sel) => {
                  if (sel && sel.length) {
                     for (let i = 0; i < this.#data_lines.length; i++)
                        this.#data_lines[i].data_source.selected.indices = [0];
                     for (let i = 0; i < this.#data_bands.length; i++)
                        this.#annotation_bands[i].visible = false;
                  }
                  else {
                     for (let i = 0; i < this.#data_lines.length; i++)
                        this.#data_lines[i].data_source.selected.indices = [];
                     for (let i = 0; i < this.#data_bands.length; i++)
                        this.#annotation_bands[i].visible = true;
                  }
               }
            },
            code: "fn(this.indices)"
         })]
      };
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   render(lim_graph, fig, axis_x, axis_y, styles, font_family) {
      if (!this.#data_scatter.data['x'] || !this.#data_scatter.data['y'])
         return;

      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = styles.styles;
      const { color_tooltip, color_tooltiptext } = styles.styles_tooltip;

      this.#renderers_lines = [];
      this.#renderers_scatter_legend = [];
      for (let i = 0; i < this.#data_lines.length; i++) {
         this.#renderers_lines.push(fig.line({
            x: { field: 'x', transform: axis_x.transform() },
            y: { field: 'y', transform: axis_y.transform() },
            source: this.#data_lines[i].data_source,
            line_dash: this.#data_lines[i].line_dash,
            line_color: this.#data_lines[i].line_color,
            line_width: this.#data_lines[i].line_width,
            line_alpha: this.#data_lines[i].line_alpha,// 0.9,
            nonselection_line_color: color_graph_data, nonselection_line_alpha: 0.4,
            hover_alpha: 1.0, hover_line_width: 1.5 * this.#data_lines[i].line_width,
            y_range_name: this.#y_range_name
         }));
         let idx = this.#data_lines[i].index;
         this.#renderers_scatter_legend.push(fig.scatter({
            fill_color: this.#data_scatter.data.fill_color[idx],
            fill_alpha: this.#data_scatter.data.fill_alpha[idx],
            line_color: null,
            marker: this.#data_scatter.data.marker[idx] == "none" ? null : this.#data_scatter.data.marker[idx],
            y_range_name: this.#y_range_name
         }));
      }
      this.#annotation_bands = [];
      for (let i = 0; i < this.#data_bands.length; i++) {
         let band = new Bokeh.Band({
            base: { field: 'x', transform: axis_x.transform() },
            lower: axis_y.range_scale === 'linear' ? 0 : Number.EPSILON,
            upper: { field: 'y', transform: axis_y.transform() },
            source: this.#data_bands[i].data_source,
            level: 'underlay',
            fill_alpha: this.#data_bands[i].fill_opacity, fill_color: this.#data_bands[i].fill_color, line_color: null,
            y_range_name: this.#y_range_name
         });
         this.#annotation_bands.push(band);
         fig.add_layout(band);
      }
      this.#renderer_scatter = fig.scatter({
         x: { field: 'x', transform: axis_x.transform() },
         y: { field: 'y', transform: axis_y.transform() },
         size: { field: 'size' },
         source: this.#data_scatter,
         view: new Bokeh.CDSView({
            filter: new Bokeh.InversionFilter({ operand: new Bokeh.GroupFilter({ column_name: "marker", group: "none" }) })
         }),
         fill_color: this.#color_mapper ? { field: 'color_value', transform: this.#color_mapper } : { field: 'fill_color' },
         fill_alpha: { field: 'fill_alpha' }, line_color: null,
         marker: { field: 'marker' },
         selection_fill_alpha: 0.9, selection_line_color: "black", selection_line_alpha: 1,
         nonselection_fill_color: color_graph_data, nonselection_fill_alpha: 0.4, nonselection_line_color: null,
         y_range_name: this.#y_range_name
      });
      this.#annotation_labels = new Bokeh.LabelSet({
         x: { field: 'x', transform: axis_x.transform() },
         y: { field: 'y', transform: axis_y.transform() },
         y_offset: { field: 'size' },
         text: { field: 'label' },
         source: this.#data_scatter,
         text_color: color_text,
         text_font: font_family,
         text_font_size: "10px",
         text_align: 'center',
         text_baseline: 'middle',
         y_range_name: this.#y_range_name
      });
      fig.add_layout(this.#annotation_labels);
      for (let i = 0; i < this.#data_error_bars.length; i++) {
         let whisker_head = new Bokeh.TeeHead({
            line_color: this.#data_error_bars[i].line_color,
            line_alpha: 0.7,
            line_width: this.#data_error_bars[i].whisker_line_width,
            size: 10
         });
         let whisker = new Bokeh.Whisker({
            base: { field: 'base', transform: axis_x.transform() },
            lower: { field: 'lower', transform: axis_y.transform() },
            upper: { field: 'upper', transform: axis_y.transform() },
            source: this.#data_error_bars[i].data_source,
            line_color: this.#data_error_bars[i].line_color,
            line_alpha: 0.7,
            line_width: this.#data_error_bars[i].line_width,
            lower_head: whisker_head,
            upper_head: whisker_head,
            y_range_name: this.#y_range_name
         });
         fig.add_layout(whisker);
      }
      if (this.#color_mapper && (this.#size_bar_properties?.colored ?? false) === false) {
         const range = this.#color_mapper.high - this.#color_mapper.low;
         fig.add_layout(new Bokeh.ColorBar({
            color_mapper: this.#color_mapper,
            border_line_width: 0,
            major_tick_line_alpha: 0,
            major_label_text_font: font_family,
            background_fill_color: color_base,
            major_label_text_color: color_text,
            border_line_color: color_midlight,
         }), 'right');
         fig.add_layout(new Bokeh.LinearAxis({
            axis_label: this.#color_map_title,
            axis_label_text_font: font_family,
            axis_label_text_font_style: 'normal',
            axis_label_text_color: color_text,
            ticker: new Bokeh.AdaptiveTicker({ desired_num_ticks: 0, num_minor_ticks: 0 }),
            axis_label_standoff: 0,
            major_tick_out: 0,
            minor_tick_out: 0,
            axis_line_alpha: 0
         }), 'right');
      }

      let tooltipText = `
         <div><div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 6px; margin: -6px -6px -15px -6px;">
            <table style="font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
               <tr>
                  <td rowspan="4" style="background: @line_color; width: 7px"></td>
                  <td rowspan="4" style="width: 2px">
                  <td colspan="4" style="font-size: 11px; border-bottom: 1px solid @line_color;"><b>@y_title</b></td>
               </tr>
               <tr><td><b>X:</b></td><td class="text-right">$x</td><td>@x_units</td><td width="25%"></td></tr>
               <tr><td><b>Y:</b></td><td class="text-right">$y</td><td>@y_units</td><td></td></tr>
            </table>
         </div></div>`; //`
      if (this.#data_lines[0]?.data_source.data.group_title)
         tooltipText = `
            <div><div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 6px; margin: -6px -6px -15px -6px;">
               <table style="font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
                  <tr>
                     <td rowspan="4" style="background: @line_color; width: 7px"></td>
                     <td rowspan="4" style="width: 2px">
                     <td colspan="4" style="font-size: 11px;"><b>@y_title</b></td>
                  </tr>
                  <tr><td colspan="4" style="font-size: 10px; /*font-weight: bold;*/ font-style: italic; border-bottom: 1px solid @line_color;">@group_title</td></tr>
                  <tr><td><b>&asymp;X:</b></td><td class="text-right">$x</td><td>@x_units</td><td width="25%"></td></tr>
                  <tr><td><b>&asymp;Y:</b></td><td class="text-right">$y</td><td>@y_units</td><td></td></tr>
               </table>
            </div></div>`; //`

      fig.add_tools(new Bokeh.HoverTool({
         renderers: this.#renderers_lines,
         tooltips: tooltipText,
         line_policy: 'none',
         point_policy: 'none',
         attachment: 'above'
      }));
      // fig.add_tools(new Bokeh.TapTool({
      //    description: "tap",
      //    renderers: this.#renderers_lines,
      //    //tooltips: tooltipText,
      //    //attachment: this.#y_range_name == 'default' ? 'left' : 'right'
      // }));

      tooltipText = `
      <div><div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 6px; margin: -6px -6px -15px -6px;">
      <table style="font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
      <tr>
         <td rowspan="5" style="background: @fill_color; width: 7px"></td>
         <td rowspan="5" style="width: 2px">
         <td colspan="4" style="font-size: 11px; border-bottom: 1px solid @fill_color;"><b>@ident</b></td>
      </tr>
      <tr><td><b>X:</b></td><td>@x_title</td><td class="text-right">@x</td><td>@x_units</td></tr>
      <tr><td><b>Y:</b></td><td>@y_title</td><td class="text-right">@y</td><td>@y_units</td></tr>`;
      if (this.#data_scatter.data.color_title)
         tooltipText += `<tr><td><b>Color:</b></td><td>@color_title</td><td class="text-right">@color_value</td><td>@color_units</td></tr>`;
      if (this.#data_scatter.data.size_title)
         tooltipText += `<tr><td><b>Size:</b></td><td>@size_title</td><td class="text-right">@size_value</td><td>@size_units</td></tr>`;
      tooltipText += `</table></div></div>`; //`

      fig.add_tools(new Bokeh.HoverTool({
         renderers: [this.#renderer_scatter],
         tooltips: tooltipText,
         //attachment: this.#y_range_name == 'default' ? 'left' : 'right'
      }));
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get selection() {
      let indices = this.#data_scatter.selected.indices.filter(index => index !== -1);
      let row_ids = this.#data_scatter?.data?.row_id ?? [];
      let selected = indices.map(value => row_ids[value]);
      return Array.from(new Set(selected));
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set selection(value) {
      if (!value || !value.length) {
         if (this.#data_scatter?.selected)
            this.#data_scatter.selected.indices = [];
      }
      else {
         let row_ids = this.#data_scatter?.data?.row_id ?? [];
         let new_selection = [];
         for (let i = 0; i < value.length; i++) {
            const index = value[i];
            let j = -1;
            while ((j = row_ids.indexOf(index, j + 1)) >= 0)
               new_selection.push(j);
         }
         if (new_selection.length)
            this.#data_scatter.selected.indices = new_selection;
         else
            this.#data_scatter.selected.indices = [-1];
      }
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get selectionChanged() {
      return this.#selectionChanged;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   legendItems() {
      let items = [];
      for (let i = 0; i < this.#data_legend.length; i++) {
         let index = this.#data_legend[i].index;
         let ri = this.#data_lines.findIndex(element => element.index === index);
         let renderers = ri != -1 ? [this.#renderers_scatter_legend[ri], this.#renderers_lines[ri]] : [this.#renderer_scatter];
         items.push(new Bokeh.LegendItem({
            label: this.#data_legend[i].label,
            renderers: renderers,
            index:  ri != -1 ? null : index
         }));
      }
      return items;
   }

   set ordered(val) { this.#ordered = val; }
   set default_visibility_line(val) { this.#default_visibility_line = val; }
   set default_visibility_marker(val) { this.#default_visibility_marker = val; }

   get range_x_min() { return this.#range_x_min; }
   get range_x_max() { return this.#range_x_max; }
   get range_y_min() { return this.#range_y_min; }
   get range_y_max() { return this.#range_y_max; }

   get size_bar_properties() { return this.#size_bar_properties; }

   get renderer_scatter() { return this.#renderer_scatter; }
}

/*___________________________________________________________________________*/
class ErrorBarColumn {
   #column_id
   #source_column_id
   #line_color
   #line_width
   #whisker_line_width

   constructor(column_id, pars) {
      this.#column_id = column_id;
      this.#source_column_id = pars?.source_column ?? "";
      this.#line_color = pars?.line_color ?? "";
      this.#line_width = pars?.line_width ?? 1;
      this.#whisker_line_width = pars?.whisker_line_width ?? 1;

      if (this.#line_color == "transparent" || this.#line_color == "#00000000")
         this.#line_color = "";
   }

   get column_id() { return this.#column_id; }
   get source_column_id() { return this.#source_column_id; }
   get line_color() { return this.#line_color; }
   get line_width() { return this.#line_width; }
   get whisker_line_width() { return this.#whisker_line_width; }
}

/*___________________________________________________________________________*/
class ErrorBars {
   #error_bars

   constructor(pars) {
      this.#error_bars = [];
      for (const [column_id, params] of Object.entries(pars))
         this.#error_bars.push(new ErrorBarColumn(column_id, params));
   }

   at(column_id) {
      for (let i = 0; i < this.#error_bars.length; i++)
         if (this.#error_bars[i].column_id == column_id)
            return this.#error_bars[i];
      return new ErrorBarColumn(column_id, {});
   };
}

/*___________________________________________________________________________*/
class SeriesStyleColumn {
   #is_linechart

   #column_id
   #color
   #line_dash
   #line_width
   #fill_color
   #fill_opacity
   #marker_type
   #label_type

   #marker_size
   #opacity
   #line_color
   #line_opacity

   constructor(column_id, pars, is_linechart = false) {
      this.#is_linechart = is_linechart;
      this.#column_id = column_id;
      this.#color = pars?.color ?? "";
      this.#line_dash = pars?.line_dash ?? "";
      this.#line_width = pars?.line_width ?? 3;
      this.#fill_color = pars?.fill_color ?? "";
      this.#fill_opacity = pars?.fill_opacity ?? 0.0;
      this.#marker_type = pars?.marker ?? "";
      this.#label_type = pars?.label_type ?? "";
      this.#marker_size = pars?.marker_size ?? 10;
      this.#opacity = pars?.opacity ?? 0.7;
      this.#line_color = pars?.line_color ?? "";
      this.#line_opacity = pars?.line_opacity ?? 0.7;

      if (this.#color == "transparent" || this.#color == "#00000000")
         this.#color = "";
      if (this.#line_color == "transparent" || this.#line_color == "#00000000")
         this.#line_color = "";
      if (this.#fill_color == "transparent" || this.#fill_color == "#00000000")
         this.#fill_color = "";
   }

   get primary_color() { return this.#is_linechart ? this.#line_color : this.#color; }

   get column_id() { return this.#column_id; }
   get color() { return this.#color ? this.#color : this.primary_color; }
   get line_dash() { return this.#line_dash; }
   get line_width() { return this.#line_width; }
   get fill_color() { return this.#fill_color ? this.#fill_color : this.primary_color; }
   get fill_opacity() { return this.#fill_opacity; }
   get marker_type() { return this.#marker_type; }
   get label_type() { return this.#label_type; }
   get marker_size() { return this.#marker_size; }
   get opacity() { return this.#opacity; }
   get line_color() { return this.#line_color ? this.#line_color : this.primary_color; }
   get line_opacity() { return this.#line_opacity; }
}

/*___________________________________________________________________________*/
class SeriesStyle {
   #series_style
   #is_linechart

   constructor(pars, is_linechart = false) {
      this.#is_linechart = is_linechart;
      this.#series_style = [];
      for (const [column_id, params] of Object.entries(pars))
         this.#series_style.push(new SeriesStyleColumn(column_id, params, is_linechart));
   }

   at(column_id) {
      for (let i = 0; i < this.#series_style.length; i++)
         if (this.#series_style[i].column_id == column_id)
            return this.#series_style[i];
      return new SeriesStyleColumn(column_id, {}, this.#is_linechart);
   };
}

/*___________________________________________________________________________*/
class LimGraphAxis {
   #title_visible
   #title_value

   #labels_visible
   #labels_format
   #labels_precision

   #range_min_type
   #range_min_value
   #range_max_type
   #range_max_value
   #range_scale
   #range_reversed

   #step_major_type
   #step_major_value
   #step_major_tick
   #step_major_grid
   #step_minor_type
   #step_minor_value
   #step_minor_tick
   #step_minor_grid

   constructor(pars) {
      this.#title_visible = pars?.title_visible ?? true;
      this.#title_value = pars?.title_value ?? '';

      this.#labels_visible = pars?.labels_visible ?? true;
      this.#labels_format = pars?.labels_format ?? '';
      this.#labels_precision = pars?.labels_precision ?? 3;

      this.#range_min_type = pars?.range_min_type ?? 'auto';
      this.#range_min_value = pars?.range_min_value ?? null;
      this.#range_max_type = pars?.range_max_type ?? 'auto';
      this.#range_max_value = pars?.range_max_value ?? null;
      this.#range_scale = pars?.range_scale ?? 'linear';
      this.#range_reversed = pars?.range_reversed ?? false;

      this.#step_major_type = pars?.step_major_type ?? 'auto';
      this.#step_major_value = LimGraphBokeh.tryNumber(pars?.step_major_value);
      this.#step_major_tick = pars?.step_major_tick ?? true;
      this.#step_major_grid = pars?.step_major_grid ?? true;
      this.#step_minor_type = pars?.step_minor_type ?? 'auto';
      this.#step_minor_value = LimGraphBokeh.tryNumber(pars?.step_minor_value);
      this.#step_minor_tick = pars?.step_minor_tick ?? true;
      this.#step_minor_grid = pars?.step_minor_grid ?? false;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createAxis(name) {
      let newAxis;
      switch (this.#range_scale) {
         case "log":
            newAxis = new Bokeh.LogAxis({ y_range_name: name });
         case "linear":
         default:
            newAxis = new Bokeh.LinearAxis({ y_range_name: name });
      }
      this.formatAxis(newAxis)
      return newAxis;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   formatAxis(axis) {
      axis.ticker = this.createTicker();
      axis.formatter = this.createFormatter();

      if (!this.#step_major_tick)
         axis.major_tick_line_color = null;
      if (!this.#step_minor_tick)
         axis.minor_tick_line_color = null;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createGridSecondary(name, dimension, axis) {
      let grid = new Bokeh.Grid({
         dimension: dimension,
         axis: axis,
         y_range_name: name
      });
      this.formatGrid(grid);
      return grid;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   formatGrid(grid) {
      grid.ticker = this.createTicker();

      if (!this.#step_major_grid)
         grid.grid_line_color = null;
      if (!this.#step_minor_grid)
         grid.minor_grid_line_color = null;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createRange(lower, upper, lower_spacing = true, upper_spacing = true) {
      if (lower === null || upper === null) {
         lower = 0;
         upper = 100;
      }
      if (lower === 0 && upper === 0) {
         lower = 0;
         upper = 100;
      }
      if (!isFinite(lower) || !isFinite(upper)) {
         lower = 0;
         upper = 100;
      }
      let c = (this.#labels_format == "time") ? 1000 : 1;
      let min = lower * c;
      let max = upper * c;

      if(min >= max) {
         if(this.#range_min_type !== "fixed")
            min = max - Math.pow(10, Math.floor(Math.log10(Math.abs(max))));
         else if(this.#range_max_type !== "fixed")
            max = min + Math.pow(10, Math.floor(Math.log10(Math.abs(min))));
      }

      if (this.#range_scale == "linear") {
         let border = (max - min) * 0.05;
         if (lower_spacing && this.#range_min_type != 'fixed')
               min -= border;
         if (upper_spacing && this.#range_max_type != 'fixed')
            max += border;
      }
      else if (this.#range_scale == "log") {
         min = min > 0 ? min : 0.001//Number.EPSILON;
         max = max > 0 ? max : 0.001//Number.EPSILON;

         let border = (Math.log10(max) - Math.log10(min)) * 0.05;
         if (lower_spacing && this.#range_min_type != 'fixed')
            min = Math.max(Number.EPSILON, Math.pow(10, Math.log10(min) - border));
         if (upper_spacing && this.#range_max_type != 'fixed')
            max = Math.max(Number.EPSILON, Math.pow(10, Math.log10(max) + border));
      }

      let range =  new Bokeh.Range1d({
         start: !this.#range_reversed ? min : max,
         end: !this.#range_reversed ? max : min,
      });
      range.js_property_callbacks = {
         "change:end": [new Bokeh.CustomJS({            
            args: {
               range: range,
               fn: (range) => {
                  if (!Number.isFinite(range.end))
                     range.end = 1e308;
                  if (!Number.isFinite(range.start))
                     range.start = -1e308;
               }
            },
            code: "fn(range)"
         })]
      };
      return range;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createScale() {
      switch (this.#range_scale) {
         case "log":
            return new Bokeh.LogScale();
         case "linear":
         default:
            return new Bokeh.LinearScale();
      }
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createTicker() {
      let tick_interval = this.#step_major_type == 'fixed' ? this.#step_major_value : null;
      let minor_count = this.#step_minor_type == 'fixed' ? this.#step_minor_value : null;
      if (tick_interval && minor_count)
         minor_count = Math.floor(tick_interval / minor_count);

      if (this.#labels_format == 'time') {
         return new Bokeh.DatetimeTicker({});
      }

      if (tick_interval || minor_count || this.#range_scale == "linear") {
         return new Bokeh.BasicTicker({
            max_interval: tick_interval,
            min_interval: tick_interval ?? 0,
            num_minor_ticks: minor_count ?? 5
         });
      }

      return new Bokeh.LogTicker({
         max_interval: tick_interval,
         min_interval: tick_interval ?? 0,
         num_minor_ticks: minor_count ?? 5
      });
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   createFormatter() {
      if (!this.#labels_visible)
         return new Bokeh.PrintfTickFormatter({ format: '' });

      switch (this.#labels_format) {
         case 'number':
         default: {
            if (this.#range_scale === "log")
               return new Bokeh.LogTickFormatter({});
            return new Bokeh.BasicTickFormatter({
               use_scientific: false,
               precision: Number(this.#labels_precision)
            });
         }
         case 'scientific': {
            return new Bokeh.BasicTickFormatter({
               power_limit_low: 0,
               power_limit_high: 0,
               precision: Number(this.#labels_precision)
            });
         }
         case 'time': {
            return new Bokeh.CustomJSTickFormatter({
               args: {
                  //ToDo: handle display precision
                  fn: (ticks, tick) => {
                     if (!this.tickStep) {
                        if (ticks.length > 2) {
                           this.tickStep = ticks[1] - ticks[0];
                           if (this.tickStep / 86400000 >= 1)
                              this.precision = "d";
                           else if (this.tickStep / 3600000 >= 1)
                              this.precison = "h";
                           else if (this.tickStep / 60000 >= 1)
                              this.precision = "m";
                           else if (this.tickStep / 1000 >= 1)
                              this.precision = "s";
                           else
                              this.precision = "ms"
                        }
                     }

                     let ng = "";
                     if (tick < 0) {
                        ng = "-";
                        tick = - tick;
                     }
                     let d = Math.floor(tick / 86400000);
                     if (d >= 7)
                        return ng + d.toFixed(0) + " days";
                     let ms = Math.floor(tick % 1000);
                     tick = Math.floor(tick / 1000);
                     let h = Math.floor(tick / 3600);
                     tick = Math.floor(tick % 3600);
                     let m = Math.floor(tick / 60);
                     let s = Math.floor(tick % 60);
                     if (h != 0)
                        return ng + h.toFixed(0) + ":" + m.toFixed(0).padStart(2, "0") + ":" + s.toFixed(0).padStart(2, "0");
                     else if (m != 0)
                        return ng + m.toFixed(0) + ":" + s.toFixed(0).padStart(2, "0");
                     else if (s != 0)
                        return ng + s.toFixed(0) + "s";
                     else if (ms != 0)
                        return ng + "0." + ms.toFixed(0).padStart(3, "0") + "s";
                     else
                        return 0;
                  }
               },
               code: 'return fn(ticks, tick);'
            });
         }
      }
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   transform() {
      if (this.#labels_format != "time")
         return null;

      const code = `
         let new_xs = new Array(xs.length)
         for(let i = 0; i < xs.length; i++)
            new_xs[i] = xs[i] *1000.0;
         return new_xs;`;
      return new Bokeh.CustomJSTransform({ v_func: code });
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   timeToLabel(time) {
      let tick = parseFloat(time);
      let ng = "";
      if (tick < 0) {
         ng = "-";
         titickme = - tick;
      }
      let d = Math.floor(tick / 86400000);
      if (d >= 7)
         return ng + d.toFixed(0) + " days";
      let ms = Math.floor(tick % 1000);
      tick = Math.floor(tick / 1000);
      let h = Math.floor(tick / 3600);
      tick = Math.floor(tick % 3600);
      let m = Math.floor(tick / 60);
      let s = Math.floor(tick % 60);
      if (h != 0)
         return ng + h.toFixed(0) + ":" + m.toFixed(0).padStart(2, "0") + ":" + s.toFixed(0).padStart(2, "0");
      else if (m != 0)
         return ng + m.toFixed(0) + ":" + s.toFixed(0).padStart(2, "0");
      else if (s != 0)
         return ng + s.toFixed(0) + "s";
      else if (ms != 0)
         return ng + "0." + ms.toFixed(0).padStart(3, "0") + "s";
      else
         return 0;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   formatValue(value) {
      switch (this.#labels_format) {
         default:
            return value;
         case 'number':
            return parseFloat(value).toFixed(this.#labels_precision);
         case 'scientific':
            return parseFloat(value).toExponential(this.#labels_precision);
         case 'time':
            return this.timeToLabel(value).toString();
      }
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   formatFactor(factors) {
      let factor_mapper = {
         factor_to_index: new Map(),
         factor_to_label: new Map(),
         index_to_label: new Map()
      }
      switch (this.#labels_format) {
         default:
            for (let i = 0; i < factors.length; i++) {
               factor_mapper.factor_to_index.set(factors[i], i);
               factor_mapper.factor_to_label.set(factors[i], factors[i]);
               factor_mapper.index_to_label.set(i, factors[i]);
               factors[i] = i;
            }
            return factor_mapper;
         case 'number':
            for (let i = 0; i < factors.length; i++) {
               const label = parseFloat(factors[i]).toFixed(this.#labels_precision);
               factor_mapper.factor_to_index.set(factors[i], i);
               factor_mapper.factor_to_label.set(factors[i], label);
               factor_mapper.index_to_label.set(i, label);
               factors[i] = i;
            }
            return factor_mapper;
         case 'scientific':
            for (let i = 0; i < factors.length; i++) {
               const label = parseFloat(factors[i]).toExponential(this.#labels_precision);
               factor_mapper.factor_to_index.set(factors[i], i);
               factor_mapper.factor_to_label.set(factors[i], label);
               factor_mapper.index_to_label.set(i, label);
               factors[i] = i;
            }
            return factor_mapper;
         case 'time':
            for (let i = 0; i < factors.length; i++) {
               const label = this.timeToLabel(factors[i]).toString();
               factor_mapper.factor_to_index.set(factors[i], i);
               factor_mapper.factor_to_label.set(factors[i], label);
               factor_mapper.index_to_label.set(i, label);
               factors[i] = i;
            }
            return factor_mapper;
      }
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   get title_visible() { return this.#title_visible; }
   get title_value() { return this.#title_value; }
   get labels_visible() { return this.#labels_visible }
   get labels_format() { return this.#labels_format; }
   get labels_precision() { return this.#labels_precision; }
   get range_min_type() { return this.#range_min_type; }
   get range_min_value() { return this.#range_min_value; }
   get range_max_type() { return this.#range_max_type; }
   get range_max_value() { return this.#range_max_value; }
   get range_scale() { return this.#range_scale; }
   get range_reversed() { return this.#range_reversed; }
   get step_major_type() { return this.#step_major_type; }
   get step_major_value() { return this.#step_major_value; }
   get step_major_tick() { return this.#step_major_tick; }
   get step_major_grid() { return this.#step_major_grid; }
   get step_minor_type() { return this.#step_minor_type; }
   get step_minor_value() { return this.#step_minor_value; }
   get step_minor_tick() { return this.#step_minor_tick; }
   get step_minor_grid() { return this.#step_minor_grid; }

   set range_scale(val) { return this.#range_scale = val; }
}

/*___________________________________________________________________________*/
class LimGraphV2 extends LimGraphControls {
   #handle

   #palette_series
   #palette_color_by
   #series_style
   #error_bars

   #label_type

   #axis_x
   #axis_y
   #axis_yr
   #axis_color

   #data_y_default
   #data_y_right

   #inner_selection

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.#palette_series = pars?.palette_series ? pars.palette_series : "";
      this.#palette_color_by = pars?.palette_color_by ? pars.palette_color_by : "";
      this.#series_style = new SeriesStyle(pars?.series_style ?? [], this instanceof LimGraphLinechartV2);
      this.#error_bars = new ErrorBars(pars?.error_bars ?? []);

      this.#label_type = pars?.label_type ?? "";

      this.#axis_x = new LimGraphAxis(pars?.axis_x ?? null);
      this.#axis_y = new LimGraphAxis(pars?.axis_y ?? null);
      this.#axis_yr = new LimGraphAxis(pars?.axis_yr ?? null);
      this.#axis_color = pars?.axis_color ?? { };

      this.#data_y_default = new DataSource('default', 'default');
      this.#data_y_right = new DataSource('default', 'y_right');

      this.#inner_selection = false;

      this.onRowSelectionChanged = that => { this.setSelection(); };

      this.update();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set ordered(val) {
      this.#data_y_default.ordered = val;
      this.#data_y_right.ordered = val;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set default_visibility_line(val) {
      this.#data_y_default.default_visibility_line = val;
      this.#data_y_right.default_visibility_line = val;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   set default_visibility_marker(val) {
      this.#data_y_default.default_visibility_marker = val;
      this.#data_y_right.default_visibility_marker = val;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   update() {
      //TODO check if new or old and something has changed
      this.fetchAndFillGraphDataSource();

      this.updateFeatureList();
      this.updateGraph();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   updateGraph() {
      if (!this.tableData)
         return;

      this.setData();
      this.drawScatter();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setSelection() {
      if (!this.#inner_selection) {
         this.#data_y_default.selection = this.rowSelection;
         this.#data_y_right.selection = this.rowSelection;
      }
      else
         this.#inner_selection = false;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setData() {
      if (!this.tableData)
         return;

      if (!this.selection_y_multi_visible)
         this.yAxisMultiOptionValue = this.yAxisColIds;
      if (!this.selection_yr_multi_visible)
         this.yAxisRightMultiOptionValue = this.yAxisRightColIds;

      let colorizer = new Colorizer(this.#palette_series, this.#palette_color_by, this.#axis_color ?? {});

      this.#data_y_default.createData({
         table_data: this.tableData,
         col_ids: {
            x: this.xAxisColId,
            y: this.yAxisColIds,
            color: this.colorAxisColId,
            size: this.sizeAxisColId
         },
         colorizer: colorizer,
         series_style: this.#series_style,
         error_bars: this.#error_bars,
         default_label_type: this.#label_type,
         col_ids_visible: this.yAxisMultiOptionValue,
         autoscale: this.autoScaleOptionValue
      });
      this.#data_y_right.createData({
         table_data: this.tableData,
         col_ids: {
            x: this.xAxisColId,
            y: this.yAxisRightColIds
         },
         colorizer: colorizer,
         series_style: this.#series_style,
         error_bars: this.#error_bars,
         default_label_type: this.#label_type,
         col_ids_visible: this.yAxisRightMultiOptionValue,
         autoscale: this.autoScaleOptionValue
      });

      this.setSelection();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async drawScatter() {
      this.containerMessage = "";
      if (!this.tableData || !this.containerGraph)
         return;

      const x_index = this.tableData.colIndexById(this.xAxisColId);
      if (x_index < 0) {
         this.containerMessage = "Missing X Axis column";
         return;
      }
      const y_indexes = this.yAxisColIds.map(id => this.tableData.colIndexById(id));
      if (y_indexes.includes(-1)) {
         this.containerMessage = "Invalid Y Axis Left column";
         return;
      }
      const yr_indexes = this.yAxisRightColIds.map(id => this.tableData.colIndexById(id));
      if (yr_indexes.includes(-1)) {
         this.containerMessage = "Invalid Y Axis Right column";
         return;
      }
      if(!y_indexes.length && !yr_indexes.length) {
         this.containerMessage = "Missing Y Axis column";
         return;
      }

      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();

      let xAxisMeta = this.axisMetadata({
         col_ids: this.xAxisColId,
         axis_settings: this.#axis_x,
         autoscale: this.autoScaleOptionValue,
         data_min: this.#data_y_default.range_x_min ?? this.#data_y_right.range_x_min,
         data_max: this.#data_y_default.range_x_max ?? this.#data_y_right.range_x_max
      });
      let yAxisMeta = this.axisMetadata({
         col_ids: this.yAxisColIds,
         axis_settings: this.#axis_y,
         autoscale: this.autoScaleOptionValue,
         data_min: this.#data_y_default.range_y_min,
         data_max: this.#data_y_default.range_y_max
      });
      let yAxisRightMeta = this.axisMetadata({
         col_ids: this.yAxisRightColIds,
         axis_settings: this.#axis_yr,
         autoscale: this.autoScaleOptionValue,
         data_min: this.#data_y_right.range_y_min,
         data_max: this.#data_y_right.range_y_max
      });

      let clip_x_axis = false;
      if (this instanceof LimGraphLinechartV2) {
         let has_markers = false;
         let has_error_bars = false;
         let yidxs = y_indexes.concat(yr_indexes);
         for (let i = 0; i < yidxs.length; i++) {
            const id = this.tableData.colIdAt(yidxs[i]);
            let marker_type = this.#series_style.at(id).marker_type;
            let error_bar_id = this.#error_bars.at(id).source_column_id;
            has_markers |= (marker_type.length && marker_type !== "none");
            has_error_bars |= error_bar_id && this.tableData.colIndexById(error_bar_id) >= 0;
            if (has_markers || has_error_bars)
               break;
         }
         clip_x_axis = !has_markers && !has_error_bars;
      }

      const fig = Bokeh.Plotting.figure({
         title: this.title,
         x_range: this.#axis_x.createRange(xAxisMeta.min, xAxisMeta.max, !clip_x_axis, !clip_x_axis),
         x_scale: this.#axis_x.createScale(),
         y_range: this.#axis_y.createRange(yAxisMeta.min, yAxisMeta.max),
         y_scale: this.#axis_y.createScale(),
         tools: this.graphTools.filter(item => item !== "hover").join(","),
         sizing_mode: "stretch_both"
      });

      this.styleGraphFigure(fig, xAxisMeta.title, yAxisMeta.title);

      //enable zooming out after hitting bounds
      if (fig.toolbar.active_scroll) {
         fig.toolbar.active_scroll.maintain_focus = false;
      }

      this.#axis_x.formatAxis(fig.xaxis);
      this.#axis_x.formatGrid(fig.xgrid);
      this.#axis_y.formatAxis(fig.yaxis);
      this.#axis_y.formatGrid(fig.ygrid);

      if (!this.yAxisColIds.length) {
         fig.yaxis.visible = false;
         fig.ygrid.visible = false;
      }

      if (yr_indexes.length) {
         fig.extra_y_ranges['y_right'] = this.#axis_yr.createRange(yAxisRightMeta.min, yAxisRightMeta.max);
         fig.extra_y_scales['y_right'] = this.#axis_yr.createScale();

         let yAxisRight = this.#axis_yr.createAxis('y_right');
         this.styleGraphAxis(yAxisRight, yAxisRightMeta.title);
         this.#axis_yr.formatAxis(yAxisRight);
         fig.add_layout(yAxisRight, 'right');

         let yGridRight = this.#axis_yr.createGridSecondary('y_right', 1, yAxisRight);
         this.styleGraphGridSecondary(yGridRight);
         this.#axis_yr.formatGrid(yGridRight);
         fig.add_layout(yGridRight);
      }

      let legendItems = [];

      this.#data_y_default.render(this, fig, this.#axis_x, this.#axis_y, { styles: this.styles(), styles_tooltip: this.styles_tooltip() }, this.fontFamily);
      this.#data_y_right.render(this, fig, this.#axis_x, this.#axis_yr, { styles: this.styles(), styles_tooltip: this.styles_tooltip() }, this.fontFamily);

      let renderers = [];
      if (this.#data_y_default.renderer_scatter) {
         this.#data_y_default.renderer_scatter.name = "renderer_scatter_default";
         renderers.push(this.#data_y_default.renderer_scatter)
      }
      if (this.#data_y_right.renderer_scatter) {
         this.#data_y_right.renderer_scatter.name = "renderer_scatter_right";
         renderers.push(this.#data_y_right.renderer_scatter)
      }
      fig.add_tools(new Bokeh.TapTool({
         behavior: "inspect",
         renderers: renderers,
         callback: new Bokeh.CustomJS({
            args: {
               lim_graph: this,
               fn: (cb_obj, cb_data, lim_graph) => {
                  if (!cb_data.source.inspected.indices.length)
                     return;
                  let row_id;
                  let renderer;
                  for (let i = 0; i < cb_obj.renderers.length; i++)
                     if (cb_obj.renderers[i].data_source.data?.row_id.length) {
                        row_id = cb_obj.renderers[i].data_source.data?.row_id;
                        renderer = cb_obj.renderers[i];
                        break;
                     }
                  if (!row_id)
                     return;
                  let row = row_id[cb_data.source.inspected.indices[0]];
                  cb_data.source.inspected.indices = [];

                  switch (renderer.name) {
                     case "renderer_scatter_default": {
                        this.#data_y_default.selection = [row];
                     this.#data_y_right.selection = [-1];
                     lim_graph.#inner_selection = true;
                        lim_graph.rowSelection = [row];
                        break;
                     }
                     case "renderer_scatter_right": {
                        this.#data_y_default.selection = [-1];
                        this.#data_y_right.selection = [row];
                        lim_graph.#inner_selection = true;
                        lim_graph.rowSelection = [row];
                        break;
                     }
                  }
                  // TODO: work on linechart
                  // if (!cb_data.source.inspected.indices.length)
                  //    return;
                  // if (!lim_graph.dataSource)
                  //    return;
                  // let row_id;
                  // for (let i = 0; i < cb_obj.renderers.length; i++)
                  //    if (cb_obj.renderers[i].data_source.data?.row_id.length) {
                  //       row_id = cb_obj.renderers[i].data_source.data?.row_id;
                  //       break;
                  //    }
                  // if (!row_id)
                  //    return;
                  // let row = row_id[cb_data.source.inspected.indices[0]];
                  // lim_graph.dataSource?.highlightRow?.(row);
               }
            },
            code: "fn(cb_obj, cb_data, lim_graph);"
         }),
      }));

      fig.js_event_callbacks = {
         "selectiongeometry": [new Bokeh.CustomJS({
            args: {
               lim_graph: this,
               fn: (lim_graph) => {
                  let s1 = this.#data_y_default.selection;
                  let s2 = this.#data_y_right.selection;
                  if (!s1.length && s2.length)
                     this.#data_y_default.selection = [-1];
                  if (s1.length && !s2.length)
                     this.#data_y_right.selection = [-1];
                  if (!s1.length && !s2.length) {
                     this.#data_y_default.selection = [];
                     this.#data_y_right.selection = [];
                  }
                  let union = s1.length ? (s2.length ? Array.from(new Set([...s1, ...s2])) : s1) : (s2);
                  if (JSON.stringify(union) !== JSON.stringify(lim_graph.rowSelection)) {
                     lim_graph.#inner_selection = true;
                     lim_graph.rowSelection = union;
                  }
               }
            },
            code: 'fn(lim_graph);'
         })]
      };

      if (this.legendVisible) {
         legendItems = legendItems.concat(this.#data_y_default.legendItems());
         legendItems = legendItems.concat(this.#data_y_right.legendItems());
      }

      if (this.legendOutsidePlacement) {
         // let legend = new Bokeh.Legend({
         //    items: legendItems,
         //    location: this.legendLocation, orientation: this.legendOrientation,
         // });
         // this.styleGraphLegend(legend);
         // fig.add_layout(legend, this.legendVisibility);

         const max_label_width = Math.max(0, ...legendItems.map(value => this.measureLegendLabelWidth(value.label.value))) + 10;
         let legend = new Bokeh.Legend({
            items: legendItems,
            location: 'center', orientation: this.legendOrientation,
            label_width: max_label_width
         });
         this.styleGraphLegend(legend);
         fig.add_layout(legend, this.legendVisibility);

         fig.js_property_callbacks = {
            "change:inner_width": [new Bokeh.CustomJS({
               args: {
                  legend: legend,
                  fig: fig,
                  max_label_width: max_label_width,
                  fn: (legend, fig, max_label_width) => {
                     legend.ncols = Math.max(3, Math.floor((fig.inner_width ?? 1)
                        / (max_label_width + legend.glyph_width + legend.label_standoff + legend.padding)));
                     legend.change.emit();
                  }
               },
               code: "fn(legend, fig, max_label_width)"
            })]
         };
      }
      else if (this.legendVisible) {
         fig.legend.location = this.legendLocation;
         fig.legend.items = legendItems;
         //fig.legend.click_policy = 'mute';
      }

      this.containerGraph.innerText = "";

      if (this.#data_y_default.size_bar_properties) {
         const bar_title = this.#data_y_default.size_bar_properties.title;
         const bar_min = this.#data_y_default.size_bar_properties.min;
         const bar_max = this.#data_y_default.size_bar_properties.max;
         const colored = this.#data_y_default.size_bar_properties.colored;

         let fig_size_bar = Bokeh.Plotting.figure({
            sizing_mode: "stretch_height",
            width: 31,
            min_border: 0,
            background_fill_color: color_base,
            outline_line_width: 0,
            border_fill_color: color_base,
            margin: [10, 0, 10, 0]
         });
         fig_size_bar.xaxis.visible = false;
         fig_size_bar.yaxis.visible = false;
         fig_size_bar.xgrid.visible = false;
         fig_size_bar.ygrid.visible = false;
         fig_size_bar.toolbar_location = null;
         fig_size_bar.toolbar.active_drag = null;

         fig_size_bar.y_range.range_padding = 0;
         fig_size_bar.x_range.range_padding = 0;
         fig_size_bar.scatter({ x: [0, 0], y: [bar_min, bar_max], marker: "circle", fill_color: color_text, line_color: color_text, visible: false });

         if (colored) {
            const min_width = 5;
            const max_width = 30;
            const middle = max_width / 2;

            const colorizer = new Colorizer(null, null, this.#axis_color);
            let palette256 = colorizer.color_axis_palette256();

            for (let i = 0; i < 255; i++) {
               let width1 = min_width + (max_width - min_width) * i / 255 - 2;
               let width2 = min_width + (max_width - min_width) * (i + 1) / 255 - 2;
               let y1 = bar_min + (bar_max - bar_min) * i / 255;
               let y2 = bar_min + (bar_max - bar_min) * (i + 1) / 255;

               let polygon = new Bokeh.PolyAnnotation({
                  fill_color: palette256[i], fill_alpha: 1.0,
                  line_color: palette256[i], line_alpha: 1.0, line_width: 1,
                  xs: [middle - width1 / 2, middle - width2 / 2, middle + width2 / 2, middle + width1 / 2],
                  xs_units: "screen",
                  ys: [y1, y2, y2, y1],
               });
               fig_size_bar.add_layout(polygon, 'center');
            }
         }
         else {
            let polygon = new Bokeh.PolyAnnotation({
               fill_color: color_text, fill_alpha: 0.8,
               xs: [12.5, 0, 30, 17.5],
               xs_units: "screen",
               ys: [bar_min, bar_max, bar_max, bar_min],
            });
            fig_size_bar.add_layout(polygon, 'center');
         }

         let fig_size_axis = Bokeh.Plotting.figure({
            sizing_mode: "stretch_height",
            min_width: 0,
            width: 0,
            max_width: 100,
            width_policy: "min",
            min_border: 0,
            background_fill_color: color_base,
            outline_line_width: 0,
            border_fill_color: color_base,
            margin: [10, 0, 10, 0],
         });
         fig_size_axis.xaxis.visible = false;
         fig_size_axis.yaxis.visible = false;
         fig_size_axis.xgrid.visible = false;
         fig_size_axis.ygrid.visible = false;
         fig_size_axis.toolbar_location = null;
         fig_size_axis.toolbar.active_drag = null;

         fig_size_axis.y_range.range_padding = 0;
         fig_size_axis.scatter({ x: [0, 0], y: [bar_min, bar_max], size: 0, marker: "circle", fill_color: color_text, line_color: color_text, visible: false })

         fig_size_axis.add_layout(new Bokeh.LinearAxis({
            axis_label: bar_title,
            axis_label_text_font: this.fontFamily,
            axis_label_text_font_style: 'normal',
            axis_label_text_color: color_text,
            axis_label_standoff: 0,
            major_tick_in: 0,
            major_tick_out: 0,
            minor_tick_out: 0,
            axis_line_alpha: 0,
            major_label_text_font: this.fontFamily,
            major_label_text_color: color_text,
         }), 'right');

         this.createCustomToolBar(fig);
         this.#handle = new Bokeh.Row({ children: [fig, fig_size_bar, fig_size_axis], sizing_mode: "stretch_both" });
         Bokeh.Plotting.show(this.#handle, this.containerGraph);
      }
      else {
         this.createCustomToolBar(fig);
         this.#handle = new Bokeh.Row({ children: [fig], sizing_mode: "stretch_both" });
         Bokeh.Plotting.show(this.#handle, this.containerGraph);
      }
   }

   get state() {
      return {
         xAxisColId: this.xAxisColId,
         yAxisColId: this.yAxisColId,
      }
   }

   set state(val) {
      for (let propName of Object.getOwnPropertyNames(val)) {
         this[propName] = val[propName];
      }
   }
}

/*___________________________________________________________________________*/
class LimGraphLinechartV2 extends LimGraphV2 {

   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/line_common.svg";
   static name = "Linechart";

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.ordered = true;
      this.default_visibility_line = true;
      this.default_visibility_marker = false;

      if (!this.iconres)
         this.iconres = LimGraphLinechartV2.iconres;
      if (!this.name)
         this.name = LimGraphLinechartV2.name;
   }
}

/*___________________________________________________________________________*/
class LimGraphScatterplotV2 extends LimGraphV2 {

   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/scatter_common.svg";
   static name = "Scatterplot";

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.ordered = false;
      this.default_visibility_line = false;
      this.default_visibility_marker = true;

      if (!this.iconres)
         this.iconres = LimGraphScatterplotV2.iconres;
      if (!this.name)
         this.name = LimGraphScatterplotV2.name;
   }
}

/*___________________________________________________________________________*/
class LimGraphHeatmap extends LimGraphControls {

   #gradient
   #gradient_background
   #label_type

   #axis_x
   #axis_y
   #axis_color

   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/heatmap.svg";
   static name = "Colormap";

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.#gradient = pars?.palette_series ?? "qcpGrayscale";
      this.#gradient_background = pars?.background ?? "none";
      this.#label_type = pars?.label_type ?? "none";

      this.#axis_x = new LimGraphAxis(pars?.axis_x ?? null);
      this.#axis_y = new LimGraphAxis(pars?.axis_y ?? null);
      this.#axis_color = new LimGraphAxis(pars?.axis_color ?? null);

      this.onRowSelectionChanged = that => { this.setSelection(); };

      if (!this.iconres)
         this.iconres = LimGraphHeatmap.iconres;
      if (!this.name)
         this.name = LimGraphHeatmap.name;

      this.update();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   update() {
      //TODO check if new or old and something has changed
      this.fetchAndFillGraphDataSource(); //calls updateGraph();
      this.updateFeatureList();
      //this.updateGraph();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   updateGraph() {
      if (!this.tableData)
         return;

      this.setData();
      this.drawHeatmap();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setSelection() {

   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setData() {

   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async drawHeatmap() {
      this.containerMessage = "";
      if (!this.tableData || !this.containerGraph)
         return;

      const xColIndex = this.tableData.colIndexById(this.xAxisColId);
      if (xColIndex < 0) {
         this.containerMessage = "Missing X Axis column";
         return;
      }
      const yColIndexes = this.yAxisColIds.map(id => this.tableData.colIndexById(id)).filter(index => index !== -1);
      if (!yColIndexes.length) {
         this.containerMessage = "Missing Y Axis column";
         return;
      }
      const colorColIndex = this.tableData.colIndexById(this.colorAxisColId);
      if (colorColIndex < 0) {
         this.containerMessage = "Missing Color Axis column";
         return;
      }
      const yColIndex = yColIndexes[0];

      let meta_x = this.axisMetadata({
         col_ids: this.xAxisColId,
         axis_settings: this.#axis_x,
         is_categorical: true
      });
      let meta_y = this.axisMetadata({
         col_ids: this.yAxisColIds[0],
         axis_settings: this.#axis_y,
         is_categorical: true
      });
      let meta_color = this.axisMetadata({
         col_ids: this.colorAxisColId,
         axis_settings: this.#axis_color,
         is_categorical: true
      });

      let x = this.tableData.colDataAt(xColIndex).map(value => String(value));
      let y = this.tableData.colDataAt(yColIndex).map(value => String(value));;
      let color = this.tableData.colDataAt(colorColIndex);

      let labels_x = Array.from(new Set(x));
      let labels_y = Array.from(new Set(y));

      let declType = this.tableData.colMetadataAt(xColIndex).decltype ?? "";
      if (declType === "int" || declType === "double")
         labels_x.sort((a, b) => Number(a) - Number(b));
      else
         labels_x.sort();

      if (this.#axis_x.range_reversed)
         labels_x = labels_x.reverse();

      declType = this.tableData.colMetadataAt(yColIndex).decltype ?? "";
      if (declType === "int" || declType === "double")
         labels_y.sort((a, b) => Number(a) - Number(b));
      else
         labels_y.sort();

      if (this.#axis_y.range_reversed)
         labels_y = labels_y.reverse();

      const label_formatter_x = this.#axis_x.formatFactor(labels_x);
      const label_formatter_y = this.#axis_y.formatFactor(labels_y);

      let fig = Bokeh.Plotting.figure({
         title: this.title,
         x_range: new Bokeh.DataRange1d({ range_padding: 0 }),//labels_x,//new Bokeh.FactorRange({ factors: labels_x }),
         y_range: new Bokeh.DataRange1d({ range_padding: 0 }),
         //x_scale: new Bokeh.CategoricalScale(),
         //y_scale: new Bokeh.CategoricalScale(),
         tools: this.graphTools.filter(item => item !== "hover").join(","),
         sizing_mode: "stretch_both",
         output_backend: "webgl"
      });

      fig.xaxis.ticker = new Bokeh.FixedTicker({ ticks: labels_x });
      fig.yaxis.ticker = new Bokeh.FixedTicker({ ticks: labels_y });

      fig.xaxis.major_tick_in = 0;
      fig.xaxis.major_tick_out = 0;
      fig.xaxis.minor_tick_in = 0;
      fig.xaxis.minor_tick_out = 0;
      fig.xaxis.major_label_policy = new Bokeh.NoOverlap();
      fig.yaxis.major_tick_in = 0;
      fig.yaxis.major_tick_out = 0;
      fig.yaxis.minor_tick_in = 0;
      fig.yaxis.minor_tick_out = 0;
      fig.yaxis.major_label_policy = new Bokeh.NoOverlap();

      fig.xaxis.major_label_overrides = label_formatter_x.index_to_label;
      fig.yaxis.major_label_overrides = label_formatter_y.index_to_label;

      // if (labels_x.length)
      //    fig.x_range.bounds = [labels_x[0] - 0.5, labels_x[labels_x.length - 1] + 0.5];
      // if (labels_y.length)
      //    fig.y_range.bounds = [labels_y[0] - 0.5, labels_y[labels_y.length - 1] + 0.5];

      if (!this.#axis_x.labels_visible)
         fig.xaxis.major_label_text_alpha = 0.0;
      if (!this.#axis_y.labels_visible)
         fig.yaxis.major_label_text_alpha = 0.0;

      let color_sorted = [];
      for (let i = 0; i < color.length; i++)
         color_sorted.push(color[i]);
      color_sorted.sort((a, b) => a - b);
      let color_min = color_sorted[0];
      let color_max = color_sorted[color_sorted.length - 1];

      this.styleGraphFigure(fig, meta_x.title, meta_y.title);
      this.hideGraphGrid(fig);

      let color256 = [];
      switch (this.#gradient) {
         case "bkhInferno":
            color256 = Bokeh.Palettes.Inferno256.map(value => integerToRGB(value));
            break;
         case "bkhMagma":
            color256 = Bokeh.Palettes.Magma256.map(value => integerToRGB(value));
            break;
         case "bkhPlasma":
            color256 = Bokeh.Palettes.Plasma256.map(value => integerToRGB(value));
            break;
         case "bkhViridis":
            color256 = Bokeh.Palettes.Viridis256.map(value => integerToRGB(value));
            break;
         case "bkhCividis":
            color256 = Bokeh.Palettes.Cividis256.map(value => integerToRGB(value));
            break;
         case "bkhTurbo":
            color256 = Bokeh.Palettes.Turbo256.map(value => integerToRGB(value));
            break;
         default: {
            let grad256 = nisGradients.get(this.#gradient);
            let interpolation = grad256[1];
            let domain = grad256[0].map(item => item[0]);
            let range = grad256[0].map(item => d3.rgb(item[1]));
            let scale = d3.scaleLinear()
               .domain(domain)
               .range(range)
               .interpolate(interpolation);
            for (let i = 0; i < 256; i++)
               color256.push(scale(i / 255.0));
         }
      }
      if (this.#axis_color.range_reversed)
         color256 = color256.reverse();

      let color_mapper = new Bokeh.LinearColorMapper({
         low: color_min,
         high: color_max,
         palette: color256
      });
      // let color_mapper = new Bokeh.LogColorMapper({
      //    low: color_min,
      //    high: color_max,
      //    palette: color256
      // });

      if (this.#gradient_background === "palette_start")
         fig.background_fill_color = color256[0];
      else if (this.#gradient_background === "palette_middle")
         fig.background_fill_color = color256[128];
      else if (this.#gradient_background === "palette_end")
         fig.background_fill_color = color256[255];

      let source = new Bokeh.ColumnDataSource();
      let data = {
         x: [], y: [], z: [], label: [], x_format: [], y_format: [], z_format: [], z_color: []
      };
      for (let i = 0; i < x.length; i++) {
         data.x.push(label_formatter_x.factor_to_index.get(x[i]));
         data.y.push(label_formatter_y.factor_to_index.get(y[i]));
         data.z.push(color[i]);
         data.z_color.push(color256[Math.round(255 * ((color[i] - color_min) / (color_max - color_min)))]);
         data.x_format.push(label_formatter_x.factor_to_label.get(x[i]));
         data.y_format.push(label_formatter_y.factor_to_label.get(y[i]));
         data.z_format.push(this.#axis_color.formatValue(color[i]));
         data.label.push(color[i].toLocaleString({ maximumFractionDigits: 3 }));
      }
      source.data = data;

      let renderer = fig.rect({
         x: { field: 'x' },
         y: { field: 'y' },
         width: 1.0,
         height: 1.0,
         source: source,
         line_color: { field: 'z', transform: color_mapper },
         fill_color: { field: 'z', transform: color_mapper },
         hover_line_color: "white"
      });

      const title_x = this.tableData.colMetadataAt(xColIndex).title ?? "";
      const title_y = this.tableData.colMetadataAt(yColIndex).title ?? "";
      const title_z = this.tableData.colMetadataAt(colorColIndex).title ?? "";
      const units_x = this.tableData.colMetadataAt(xColIndex).units ?? "";
      const units_y = this.tableData.colMetadataAt(yColIndex).units ?? "";
      const units_z = this.tableData.colMetadataAt(colorColIndex).units ?? "";

      const { color_tooltip, color_tooltiptext } = this.styles_tooltip();
      let tooltipText = `
         <div><div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 6px; margin: -6px -6px -15px -6px;">
         <table style="font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
            <tr>
               <td rowspan="4" style="background: @z_color; width: 7px; min-width: 7px"></td>
               <td rowspan="4" style="width: 2px; min-width: 2px;"></td>
               <td colspan="4"</td>
            </tr>
            <tr><td><b>X:</b></td><td>${title_x}</td><td class="text-right">@x_format</td><td>${units_x}</td><td width="25%"></td></tr>
            <tr><td><b>Y:</b></td><td>${title_y}</td><td class="text-right">@y_format</td><td>${units_y}</td><td></td></tr>
            <tr><td><b>Color:</b></td><td>${title_z}</td><td class="text-right">@z_format</td><td>${units_z}</td><td></td></tr>
         </table></div></div>`;
      fig.add_tools(new Bokeh.HoverTool({
         renderers: [renderer],
         tooltips: tooltipText,
         line_policy: 'none',
         point_policy: 'none',
      }));

      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();
      fig.add_layout(new Bokeh.ColorBar({
         color_mapper: color_mapper,
         ticker: this.#axis_color.createTicker(),
         formatter: this.#axis_color.createFormatter(),
         border_line_width: 0,
         major_tick_line_alpha: 0,
         major_label_text_font: this.fontFamily,
         background_fill_alpha: 0,
         major_label_text_color: color_text,
         border_line_color: color_midlight,
         major_label_text_alpha: this.#axis_color.labels_visible ? 1 : 0
      }), 'right');
      fig.add_layout(new Bokeh.LinearAxis({
         axis_label: meta_color.title,
         axis_label_text_font: this.fontFamily,
         axis_label_text_font_style: 'normal',
         axis_label_text_color: color_text,
         ticker: new Bokeh.AdaptiveTicker({ desired_num_ticks: 0, num_minor_ticks: 0 }),
         axis_label_standoff: 0,
         major_tick_out: 0,
         minor_tick_out: 0,
         axis_line_alpha: 0,
      }), 'right');

      //webgl sometimes overdraw top outline
      fig.add_layout(new Bokeh.LinearAxis({
         axis_label_standoff: 0,
         major_tick_out: 0,
         major_tick_in: 0,
         minor_tick_out: 0,
         minor_tick_in: 0,
         axis_line_color: color_text
      }), 'above');

      if (this.#label_type === 'value_x') {
         let labels_s1 = new Bokeh.LabelSet({
            x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
            y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
            x_offset: 1,
            y_offset: 1,
            text: { field: 'z_format' },
            source: source,
            text_color: "black",// color_text,
            text_font: this.fontFamily,
            text_font_size: "10px",
            text_align: 'center',
            text_baseline: 'middle',
         });
         fig.add_layout(labels_s1);
         let labels_s2 = new Bokeh.LabelSet({
            x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
            y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
            x_offset: 1,
            y_offset: -1,
            text: { field: 'z_format' },
            source: source,
            text_color: "black",// color_text,
            text_font: this.fontFamily,
            text_font_size: "10px",
            text_align: 'center',
            text_baseline: 'middle',
         });
         let labels_s3 = new Bokeh.LabelSet({
            x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
            y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
            x_offset: -1,
            y_offset: 1,
            text: { field: 'z_format' },
            source: source,
            text_color: "black",// color_text,
            text_font: this.fontFamily,
            text_font_size: "10px",
            text_align: 'center',
            text_baseline: 'middle',
         });
         fig.add_layout(labels_s3);
         let labels_s4 = new Bokeh.LabelSet({
            x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
            y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
            x_offset: -1,
            y_offset: -1,
            text: { field: 'z_format' },
            source: source,
            text_color: "black",// color_text,
            text_font: this.fontFamily,
            text_font_size: "10px",
            text_align: 'center',
            text_baseline: 'middle',
         });
         fig.add_layout(labels_s4);
         let labels = new Bokeh.LabelSet({
            x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
            y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
            y_offset: 0,
            text: { field: 'z_format' },
            source: source,
            text_color: color_text,
            text_font: this.fontFamily,
            text_font_size: "10px",
            text_align: 'center',
            text_baseline: 'middle',
         });
         fig.add_layout(labels);
      }

      this.containerGraph.innerText = "";

      this.createCustomToolBar(fig);
      //let handle = new Bokeh.Row({ children: [fig], sizing_mode: "stretch_both" });
      Bokeh.Plotting.show(fig/*handle*/, this.containerGraph);
   }
}

/*___________________________________________________________________________*/
class LimGraphBarchartV2 extends LimGraphControls {

   #axis_x
   #axis_y
   #axis_yr

   #factor_nesting
   #factor_group_visible
   #factor_ytitle_visible

   #factor_padding
   #subgroup_padding
   #group_padding

   #palette_color_by
   #label_type

   #series_style
   #error_bars

   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/barchart_double.svg";
   static name = "Barchart";

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.#axis_x = new LimGraphAxis(pars?.axis_x ?? null);
      this.#axis_y = new LimGraphAxis(pars?.axis_y ?? null);
      this.#axis_yr = new LimGraphAxis(pars?.axis_yr ?? null);

      this.#factor_nesting = pars?.factor_nesting ?? "xvalue;group;ytitle";
      this.#factor_group_visible = pars?.factor_group_visible ?? true;
      this.#factor_ytitle_visible = pars?.factor_ytitle_visible ?? true;

      this.#factor_padding = LimGraphBokeh.tryNumber(pars?.factor_padding ?? 0.0);
      this.#subgroup_padding = LimGraphBokeh.tryNumber(pars?.subgroup_padding ?? 0.5);
      this.#group_padding = LimGraphBokeh.tryNumber(pars?.group_padding ?? 1.0);

      this.#palette_color_by = pars?.palette_color_by ?? "";
      this.#label_type = pars?.label_type ?? 'none';

      this.#series_style = new SeriesStyle(pars?.series_style ?? []);
      this.#error_bars = new ErrorBars(pars?.error_bars ?? []);

      this.onRowSelectionChanged = that => { this.setSelection(); };

      if (!this.iconres)
         this.iconres = LimGraphBarchartV2.iconres;
      if (!this.name)
         this.name = LimGraphBarchartV2.name;

      this.update();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setData() {

   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setSelection() {

   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   update() {
      //TODO check if new or old and something has changed
      this.fetchAndFillGraphDataSource();

      this.updateFeatureList();
      this.updateGraph();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   updateGraph() {
      if (!this.tableData)
         return;

      this.setData();
      this.drawBarchart();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async drawBarchart() {
      this.containerMessage = "";      
      if (!this.tableData || !this.containerGraph)
         return;

      let y_ids = this.yAxisColIds;
      if (this.selection_y_multi_visible)
         y_ids = this.yAxisMultiOptionValue;
      let yr_ids = this.yAxisRightColIds;
      if (this.selection_yr_multi_visible)
         yr_ids = this.yAxisRightMultiOptionValue;

      const xColIndex = this.tableData.colIndexById(this.xAxisColId);
      const yColIndexes = y_ids.map(id => this.tableData.colIndexById(id)).filter(index => index !== -1);
      const yrColIndexes = yr_ids.map(id => this.tableData.colIndexById(id)).filter(index => index !== -1);

      if (xColIndex < 0) {
         this.containerGraph.innerText = '';
         this.containerMessage = "Missing X Axis column";
         return;
      }
      if(!yColIndexes.length && !yrColIndexes.length) {
         this.containerGraph.innerText = '';
         this.containerMessage = "Missing Y Axis column";
         return;
      }

      let x = [];
      const x_decltype = this.tableData.colMetadataAt(xColIndex).decltype ?? "";
      const is_x_numeric = x_decltype === "int" || x_decltype === "double";
      if(is_x_numeric)
         x = this.tableData.colDataAt(xColIndex);
      else
         x = this.tableData.colDataAt(xColIndex).map(value => String(value));
      if(!x.length) {
         this.containerGraph.innerText = '';
         this.containerMessage = "X Axis column does not contain any valid value ";
         return;
      }
      //let y = this.tableData.colDataAt(yColIndex).map(value => String(value));;
      //let color = this.tableData.colDataAt(colorColIndex);

      // grouping
      let groupedBy = this.tableData.groupedBy;
      const is_grouped = groupedBy.length > 0;
      let groupedBy_titles = groupedBy.map(id => this.tableData.colTitle(id));
      let groupedBy_values = [];
      let grouping = [];
      let group_begins = [];
      if (is_grouped) {
         let rows = 0;
         const groups = this.tableData.groups;
         for (let g = 0; g < groups.length; g++) {
            group_begins.push(rows);
            groupedBy_values.push(new Array(...groupedBy.map(colId => this.tableData.colData(colId, [rows]))));
            rows += groups[g].length;
            for (let i = 0; i < groups[g].length; i++)
               grouping.push(g);
         }
      }
      else {
         group_begins.push(0);
         grouping = new Array(x.length).fill(0);
      }
      let factors = Array.from(Array(group_begins.length), () => []);
      for (let i = 0; i < grouping.length; i++)
         factors[grouping[i]].push(x[i]);

      let group_names = [];
      for (let i = 0; i < groupedBy_values.length; i++) {
         let name = '';
         for (let j = 0; j < groupedBy_values[i].length; j++)
            name += (j > 0 ? ', ' : '') + groupedBy_titles[j] + ': ' + groupedBy_values[i][j];
         group_names.push(name);
      }
      if (group_names.length == 0)
         group_names.push('');

      const nesting = this.#factor_nesting.split(';');
      let iGroup = nesting.indexOf('group');
      let iTitle = nesting.indexOf('ytitle');
      let iValue = nesting.indexOf('xvalue');

      factors = factors.map(value => Array.from(new Set(value)));
      factors = factors.map((arr, arrIndex) => {
         return arr.map(value => [arrIndex, value])
      }).flat();

      if (nesting.indexOf('xvalue') < nesting.indexOf('group'))
         factors = factors.map(arr => [arr[1], arr[0]]);

      let yColIdxs = yColIndexes.concat(yrColIndexes);
      let yColMapping = yColIdxs.map((value, index) => index);
      if (!yColIdxs.length) {
         yColIdxs = [0];
         yColMapping = [0];
      }
      switch (nesting.indexOf('ytitle')) {
         case 0:
            factors = yColMapping.flatMap(idx => factors.map(factor => [idx, factor[0], factor[1]]));
            break;
         case 1:
            factors = yColMapping.flatMap(idx => factors.map(factor => [factor[0], idx, factor[1]]));
            break;
         case 2:
            factors = factors.flatMap(factor => yColMapping.map(idx => [factor[0], factor[1], idx]));
            break;
      }

      factors.sort((a, b) => {
         for (let i = 0; i < a.length && i < b.length; i++) {
            if (a[i] !== b[i]) {
               if (i == nesting.indexOf('xvalue')) {
                  if(nesting.indexOf('xvalue') > nesting.indexOf('group'))
                     return 0;
               }
               if (typeof a[i] === 'string')
                  return a[i].localeCompare(b[i]);
               return a[i] - b[i];
            }
         }
         return a.length - b.length;
      });
      if(is_x_numeric) {
         factors = factors.map(factor => factor.map((value, index) => index == nesting.indexOf('xvalue') ? String(value) : value));
         x = x.map(value => String(value));
      }

      const titles = this.tableData.colTitleList;
      if (!yColIdxs.length)
         titles = [""];
      for (let i = 0; i < factors.length; i++) {
         factors[i][iTitle] = titles[yColIdxs[factors[i][iTitle]]];
         factors[i][iGroup] = group_names[factors[i][iGroup]];
      }

      const col_count = yColIndexes.length + yrColIndexes.length;
      const group_count = Math.max(1, group_names.length);

      let color_count = col_count * group_count;
      if (this.#palette_color_by == "column")
         color_count = col_count;
      else if (this.#palette_color_by == "group")
         color_count = group_count;

      let palette = Bokeh.Palettes.Category10_10;
      if (color_count > 10)
         palette = Bokeh.Palettes.Category20_20;
      if (color_count > 20)
         palette = Bokeh.Palettes.Category20c_20.concat(Bokeh.Palettes.Category20b_20);

      if (factors[0] ? factors[0].length == 1 : false)
         factors = factors.flat();

      let col_nth = 0;

      let data_default = {
         x: [], y: [], color: [],
         x_value: [], x_title: [], x_units: [], y_title: [], y_units: [],
         label: []
      };
      let legend_labels_default = {
         label: [], index: []
      }

      let error_bars_default = [];
      let error_bars_right = [];

      let x_title = this.tableData.colTitleAt(xColIndex);
      let x_units = this.tableData.colUnitAt(xColIndex) ?? '';

      let range_y_min_all = [];
      let range_y_max_all = [];
      for (let i = 0; i < yColIndexes.length; i++) {
         let y = this.tableData.colDataAt(yColIndexes[i]);
         let y_error = [];
         let title = this.tableData.colTitleAt(yColIndexes[i])
         let units = this.tableData.colUnitAt(yColIndexes[i]) ?? '';


         const colId = this.tableData.colIdAt(yColIndexes[i]);
         let series_style = this.#series_style.at(colId);
         let error_bar = this.#error_bars.at(colId);
         if (this.tableData.colIdList.includes(error_bar.source_column_id)) {
            const error_col_id = this.tableData.colIndexById(error_bar.source_column_id);
            y_error = this.tableData.colDataAt(error_col_id);
         }

         for (let g = 0; g < group_count; g++) {
            const gBegin = group_begins[g];
            const gEnd = group_begins[g + 1] ?? x.length;

            let color = palette[(group_count * col_nth + g) % palette.length];
            if (this.#palette_color_by == "column")
               color = palette[col_nth % palette.length];
            else if (this.#palette_color_by == "group")
               color = palette[g % palette.length];
            color = series_style.color !== "" ? series_style.color : integerToRGB(color);

            legend_labels_default.label.push(title + (group_count > 1 ? ` [${group_names[g]}]` : ''));
            legend_labels_default.index.push(x.length * i + gBegin);

            for (let j = gBegin; j < gEnd; j++) {
               let arr = new Array(3);
               arr[iGroup] = group_names[grouping[j]];
               arr[iTitle] = title;
               arr[iValue] = x[j];

               data_default.x.push(arr);
               data_default.y.push(y[j]);
               data_default.color.push(color);

               data_default.x_value.push(x[j]);
               data_default.x_title.push(x_title);
               data_default.x_units.push(x_units);
               data_default.y_title.push(title);
               data_default.y_units.push(units);

               let label_type = series_style.label_type ? series_style.label_type : this.#label_type;
               switch (label_type) {
                  default:
                  case 'none': data_default.label.push(''); break;
                  case 'value_x': data_default.label.push(x[j].toLocaleString({ maximumFractionDigits: 3 })); break;
                  case 'value_y': data_default.label.push(y[j].toLocaleString({ maximumFractionDigits: 3 })); break;
                  case 'value_xy': data_default.label.push(`[${x[j].toLocaleString({ maximumFractionDigits: 3 })}; ${y[j].toLocaleString({ maximumFractionDigits: 3 })}]`); break;
               }
            }
            if (y_error.length) {
               let bar_data = { base: [], lower: [], upper: [] };
               for (let j = gBegin; j < gEnd; j++) {
                  let arr = new Array(3);
                  arr[iGroup] = group_names[grouping[j]];
                  arr[iTitle] = title;
                  arr[iValue] = x[j];
                  bar_data.base.push(arr);
                  bar_data.lower.push(y[j] - Number(y_error[j]));
                  bar_data.upper.push(y[j] + Number(y_error[j]));
               }
               error_bars_default.push({
                  index: gBegin,
                  line_color: error_bar.line_color ? error_bar.line_color : color,
                  line_width: error_bar.line_width,
                  whisker_line_width: error_bar.whisker_line_width,
                  data_source: new Bokeh.ColumnDataSource({ data: bar_data })
               });
            }
         }
         col_nth++;

         if (!y_error.length) {
            range_y_min_all.push(Math.min(...y.filter(value => Number.isFinite(value))));
            range_y_max_all.push(Math.max(...y.filter(value => Number.isFinite(value))));
         }
         else {
            range_y_min_all.push(Math.min(...y.map((value, index) => (value - y_error[index])).filter(value => Number.isFinite(value))));
            range_y_max_all.push(Math.max(...y.map((value, index) => (value + y_error[index])).filter(value => Number.isFinite(value))));
         }
      }
      let source_default = new Bokeh.ColumnDataSource();
      source_default.data = data_default;

      let data_right = {
         x: [], y: [], color: [],
         x_value: [], x_title: [], x_units: [], y_title: [], y_units: [],
         label: []
      };
      let legend_labels_right = {
         label: [], index: []
      }

      let range_yr_min_all = [];
      let range_yr_max_all = [];
      for (let i = 0; i < yrColIndexes.length; i++) {
         let y = this.tableData.colDataAt(yrColIndexes[i]);
         let y_error = [];
         let title = this.tableData.colTitleAt(yrColIndexes[i]);
         let units = this.tableData.colUnitAt(yrColIndexes[i]) ?? '';

         const colId = this.tableData.colIdAt(yrColIndexes[i]);
         let series_style = this.#series_style.at(colId);
         let error_bar = this.#error_bars.at(colId);
         if (this.tableData.colIdList.includes(error_bar.source_column_id)) {
            const error_col_id = this.tableData.colIndexById(error_bar.source_column_id);
            y_error = this.tableData.colDataAt(error_col_id);
         }

         for (let g = 0; g < group_count; g++) {
            const gBegin = group_begins[g];
            const gEnd = group_begins[g + 1] ?? x.length;

            let color = palette[(group_count * col_nth + g) % palette.length];
            if (this.#palette_color_by == "column")
               color = palette[col_nth % palette.length];
            else if (this.#palette_color_by == "group")
               color = palette[g % palette.length];
            color = series_style.color !== "" ? series_style.color : integerToRGB(color);

            legend_labels_right.label.push(title + (group_count > 1 ? ` [${group_names[g]}]` : ''));
            legend_labels_right.index.push(x.length * i + gBegin);

            for (let j = gBegin; j < gEnd; j++) {
               let arr = new Array(3);
               arr[iGroup] = group_names[grouping[j]];
               arr[iTitle] = title;
               arr[iGroup] = group_names[grouping[j]];
               arr[iValue] = x[j];

               data_right.x.push(arr);
               data_right.y.push(y[j]);
               data_right.color.push(color);

               data_right.x_title.push(x_title);
               data_right.x_units.push(x_units);
               data_right.y_title.push(title);
               data_right.y_units.push(units);
               data_right.x_value.push(x[j]);

               let label_type = series_style.label_type ?? this.#label_type;
               switch (label_type) {
                  default:
                  case 'none': data_right.label.push(''); break;
                  case 'value_x': data_right.label.push(x[j].toLocaleString({ maximumFractionDigits: 3 })); break;
                  case 'value_y': data_right.label.push(y[j].toLocaleString({ maximumFractionDigits: 3 })); break;
                  case 'value_xy': data_right.label.push(`[${x[j].toLocaleString({ maximumFractionDigits: 3 })}; ${y[j].toLocaleString({ maximumFractionDigits: 3 })}]`); break;
               }
            }
            if (y_error.length) {
               let bar_data = { base: [], lower: [], upper: [] };
               for (let j = gBegin; j < gEnd; j++) {
                  let arr = new Array(3);
                  arr[iGroup] = group_names[grouping[j]];
                  arr[iTitle] = title;
                  arr[iValue] = x[j];
                  bar_data.base.push(arr);
                  bar_data.lower.push(y[j] - Number(y_error[j]));
                  bar_data.upper.push(y[j] + Number(y_error[j]));
               }
               error_bars_right.push({
                  index: gBegin,
                  line_color: error_bar.line_color ? error_bar.line_color : color,
                  line_width: error_bar.line_width,
                  whisker_line_width: error_bar.whisker_line_width,
                  data_source: new Bokeh.ColumnDataSource({ data: bar_data })
               });
            }
         }
         col_nth++;

         if (!y_error.length) {
            range_yr_min_all.push(Math.min(...y.filter(value => Number.isFinite(value))));
            range_yr_max_all.push(Math.max(...y.filter(value => Number.isFinite(value))));
         }
         else {
            range_yr_min_all.push(Math.min(...y.map((value, index) => (value - y_error[index])).filter(value => Number.isFinite(value))));
            range_yr_max_all.push(Math.max(...y.map((value, index) => (value + y_error[index])).filter(value => Number.isFinite(value))));
         }
      }
      let source_right = new Bokeh.ColumnDataSource();
      source_right.data = data_right;

      const range_y_min = Math.min(...range_y_min_all);
      const range_y_max = Math.max(...range_y_max_all);
      const range_yr_min = Math.min(...range_yr_min_all);
      const range_yr_max = Math.max(...range_yr_max_all);

      let xAxisMeta = this.axisMetadata({
         col_ids: this.xAxisColId,
         axis_settings: this.#axis_x,
         is_categorical: true
      });
      let yAxisMeta = this.axisMetadata({
         col_ids: this.yAxisColIds,
         axis_settings: this.#axis_y,
         autoscale: this.autoScaleOptionValue,
         data_min: Math.min(this.#axis_y.range_scale == 'linear' ? 0 : 1, range_y_min),
         data_max: Math.max(this.#axis_y.range_scale == 'linear' ? 0 : 1, range_y_max)
      });
      let yAxisRightMeta = this.axisMetadata({
         col_ids: this.yAxisRightColIds,
         axis_settings: this.#axis_yr,
         autoscale: this.autoScaleOptionValue,
         data_min: Math.min(this.#axis_yr.range_scale == 'linear' ? 0 : 1, range_yr_min),
         data_max: Math.max(this.#axis_yr.range_scale == 'linear' ? 0 : 1, range_yr_max),
      })

      if(this.#axis_y.range_scale == 'linear')
         yAxisMeta.min = yAxisMeta.min > 0 ? 0 :  yAxisMeta.min;
      else
         yAxisMeta.min = yAxisMeta.min > 1 ? 1 :  yAxisMeta.min;

      if(this.#axis_yr.range_scale == 'linear')
         yAxisRightMeta.min = yAxisMeta.min > 0 ? 0 :  yAxisMeta.min;
      else
         yAxisRightMeta.min = yAxisRightMeta.min > 1 ? 1 :  yAxisRightMeta.min;

      const fig = Bokeh.Plotting.figure({
         title: this.title,
         x_range: new Bokeh.FactorRange({ factors: factors, factor_padding: this.#factor_padding, subgroup_padding: this.#subgroup_padding, group_padding: this.#group_padding }),
         x_scale: new Bokeh.CategoricalScale(),
         y_range: this.#axis_y.createRange(yAxisMeta.min, yAxisMeta.max, yAxisMeta.min < 0),
         y_scale: this.#axis_y.createScale(),
         tools: this.graphTools.filter(item => item !== "hover").join(","),
         sizing_mode: "stretch_both",
         //output_backend: "webgl"
      });

      this.styleGraphFigure(fig, xAxisMeta.title, yAxisMeta.title);
      fig.xaxis.major_label_policy = new Bokeh.NoOverlap();

      this.#axis_y.formatAxis(fig.yaxis)
      this.#axis_y.formatGrid(fig.ygrid)

      if (!this.yAxisColIds.length) {
         fig.yaxis.visible = false;
         fig.ygrid.visible = false;
      }

      if (this.yAxisRightColIds.length) {
         fig.extra_y_ranges['y_right'] = this.#axis_yr.createRange(yAxisRightMeta.min, yAxisRightMeta.max, yAxisRightMeta.min < 0);
         fig.extra_y_scales['y_right'] = this.#axis_yr.createScale();

         let yAxisRight = this.#axis_yr.createAxis('y_right');
         this.styleGraphAxis(yAxisRight, yAxisRightMeta.title);
         this.#axis_yr.formatAxis(yAxisRight);
         fig.add_layout(yAxisRight, 'right');

         let yGridRight = this.#axis_yr.createGridSecondary('y_right', 1, yAxisRight);
         this.styleGraphGridSecondary(yGridRight);
         this.#axis_yr.formatGrid(yGridRight);
         fig.add_layout(yGridRight);
      }

      let renderer_default = fig.vbar({
         x: { field: 'x' },
         top: { field: 'y' },
         bottom: this.#axis_y.range_scale === 'linear' ? 0 : Number.EPSILON,
         width: 1.0,
         source: source_default,
         color: { field: 'color' },
         fill_alpha: 0.9,
         hover_color: "white",
         y_range_name: 'default'
      });

      let renderer_right;
      if (this.yAxisRightColIds.length)
         renderer_right = fig.vbar({
            x: { field: 'x' },
            top: { field: 'y' },
            bottom: this.#axis_yr.range_scale === 'linear' ? 0 : Number.EPSILON,
            width: 1.0,
            source: source_right,
            color: { field: 'color' },
            fill_alpha: 0.9,
            hover_color: "white",
            x_range_name: 'default',
            y_range_name: 'y_right'
         });

      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();
      let labels_default = new Bokeh.LabelSet({
         x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
         y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
         y_offset: 6,
         text: { field: 'label' },
         source: source_default,
         text_color: color_text,
         text_font: this.fontFamily,
         text_font_size: "10px",
         text_align: 'center',
         text_baseline: 'middle',
         y_range_name: 'default'
      });
      fig.add_layout(labels_default);

      if (this.yAxisRightColIds.length) {
         let labels_right = new Bokeh.LabelSet({
            x: { field: 'x'/*, transform: this.#axis_x.transform()*/ },
            y: { field: 'y'/*, transform: this.#axis_y.transform()*/ },
            y_offset: 6,
            text: { field: 'label' },
            source: source_right,
            text_color: color_text,
            text_font: this.fontFamily,
            text_font_size: "10px",
            text_align: 'center',
            text_baseline: 'middle',
            y_range_name: 'y_right'
         });
         fig.add_layout(labels_right);
      }

      for (let i = 0; i < error_bars_default.length; i++) {
         let whisker_head = new Bokeh.TeeHead({
            line_color: error_bars_default[i].line_color,
            line_alpha: 0.7,
            line_width: error_bars_default[i].whisker_line_width,
            size: 10
         });
         let whisker = new Bokeh.Whisker({
            base: { field: 'base', transform: this.#axis_x.transform() },
            lower: { field: 'lower', transform: this.#axis_y.transform() },
            upper: { field: 'upper', transform: this.#axis_y.transform() },
            source: error_bars_default[i].data_source,
            line_color: error_bars_default[i].line_color,
            line_alpha: 0.7,
            line_width: error_bars_default[i].line_width,
            lower_head: whisker_head,
            upper_head: whisker_head,
            y_range_name: 'default',
            level: 'overlay'
         });
         fig.add_layout(whisker);
      }
      for (let i = 0; i < error_bars_right.length; i++) {
         let whisker_head = new Bokeh.TeeHead({
            line_color: error_bars_right[i].line_color,
            line_alpha: 0.7,
            line_width: error_bars_right[i].whisker_line_width,
            size: 10
         });
         let whisker = new Bokeh.Whisker({
            base: { field: 'base', transform: this.#axis_x.transform() },
            lower: { field: 'lower', transform: this.#axis_yr.transform() },
            upper: { field: 'upper', transform: this.#axis_yr.transform() },
            source: error_bars_right[i].data_source,
            line_color: error_bars_right[i].line_color,
            line_alpha: 0.7,
            line_width: error_bars_right[i].line_width,
            lower_head: whisker_head,
            upper_head: whisker_head,
            y_range_name: 'y_right',
            level: 'overlay'
         });
         fig.add_layout(whisker);
      }

      let legend_items = [];
      for (let i = 0; i < legend_labels_default.index.length; i++) {
         if(!Number.isFinite(legend_labels_default.index[i]))
            continue;
         legend_items.push(new Bokeh.LegendItem({
            label: legend_labels_default.label[i],
            renderers: [renderer_default],
            index: legend_labels_default.index[i]
         }));
      }
      for (let i = 0; i < legend_labels_right.index.length; i++) {
         legend_items.push(new Bokeh.LegendItem({
            label: legend_labels_right.label[i],
            renderers: [renderer_right],
            index: legend_labels_right.index[i]
         }));
      }

      let max_label_width = 0;
      for (let i = 0; i < legend_items.length; i++) {
         let legend_label = legend_items[i].label;
         max_label_width = Math.max(max_label_width, this.measureLegendLabelWidth(legend_label.value));
      }
      max_label_width += 10;

      const { color_tooltip, color_tooltiptext } = this.styles_tooltip();

      let tooltipText = `
      <div><div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 6px; margin: -6px -6px -15px -6px;">
      <table style="font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
      <tr>
         <td rowspan="3" style="background: @color; width: 7px;"></td>
         <td rowspan="3" style="width: 2px">
         <td colspan="4"></td>
      </tr>
      <tr><td><b>X:</b></td><td>@x_title</td><td class="text-right">@x_value</td><td>@x_units</td></tr>
      <tr><td><b>Y:</b></td><td>@y_title</td><td class="text-right">@y</td><td>@y_units</td></tr>
      </table></div></div>`;

      let renderers = [];
      if (renderer_default)
         renderers.push(renderer_default);
      if (renderer_right)
         renderers.push(renderer_right);

      fig.add_tools(new Bokeh.HoverTool({
         renderers: renderers,
         tooltips: tooltipText,
         line_policy: 'none',
         point_policy: 'none',
         attachment: 'horizontal',
         mode: 'vline'
      }));
      let taptool = new Bokeh.TapTool({
         renderers: renderers,
         behavior: 'inspect'
      });
      taptool.callback = new Bokeh.CustomJS({
         args: {
            fn: (cb_obj, cb_data) => {
               const sel = cb_data?.source?.inspected?.indices;
               const data = cb_data?.source?.inspect?.sender?.data;
               if (this.rowSelection && Array.isArray(sel) && Array.isArray(data?.r)) {
                  this.rowSelection = [data.r[sel[0]]];
               }
            }
         },
         code: "fn(cb_obj, cb_data);"
      });
      fig.add_tools(taptool);

      let legend;
      if (this.legendOutsidePlacement) {
         legend = new Bokeh.Legend({
            items: legend_items,
            location: 'center', orientation: this.legendOrientation,
            label_width: max_label_width
         });
         this.styleGraphLegend(legend);
         fig.add_layout(legend, this.legendVisibility);

         fig.js_property_callbacks = {
            "change:inner_width": [new Bokeh.CustomJS({
               args: {
                  legend: legend,
                  fig: fig,
                  max_label_width: max_label_width,
                  fn: (legend, fig, max_label_width) => {
                     legend.ncols = Math.max(3, Math.floor((fig.inner_width ?? 1)
                        / (max_label_width + legend.glyph_width + legend.label_standoff + legend.padding)));
                     legend.change.emit();
                  }
               },
               code: "fn(legend, fig, max_label_width)"
            })]
         };
      }
      else if (this.legendVisible) {
         fig.legend.location = this.legendLocation;
         fig.legend.items = legend_items;
      }

      this.hideGraphGridX(fig);
      this.styleGraphAxisCategory(fig.xaxis);

      if (!this.#factor_group_visible) {
         switch (iGroup) {
            case 0: fig.xaxis.group_text_font_size = "0"; break;
            case 1: fig.xaxis.subgroup_text_font_size = "0"; break;
            case 2:
               fig.xaxis.major_label_text_font_size = "0";
               fig.xaxis.major_tick_line_alpha = 0;
               break;
         }
      }
      if (!this.#factor_ytitle_visible) {
         switch (iTitle) {
            case 0: fig.xaxis.group_text_font_size = "0"; break;
            case 1: fig.xaxis.subgroup_text_font_size = "0"; break;
            case 2:
               fig.xaxis.major_label_text_font_size = "0";
               fig.xaxis.major_tick_line_alpha = 0;
               break;
         }
      }
      if(new Set(x).size <= 1) {
         switch (iValue) {
            case 0: fig.xaxis.group_text_font_size = "0"; break;
            case 1: fig.xaxis.subgroup_text_font_size = "0"; break;
            case 2:
               fig.xaxis.major_label_text_font_size = "0";
               fig.xaxis.major_tick_line_alpha = 0;
               break;
         }
      }

      if (!this.#axis_x.labels_visible) {
         fig.xaxis.major_tick_line_alpha = 0;
         fig.xaxis.major_label_text_font_size = "0";
         fig.xaxis.group_text_font_size = "0";
         fig.xaxis.subgroup_text_font_size = "0";
      }

      this.containerGraph.innerText = '';
      this.createCustomToolBar(fig);
      let handle = new Bokeh.Row({ children: [fig], sizing_mode: "stretch_both" });
      Bokeh.Plotting.show(handle, this.containerGraph);
   }
}

/*___________________________________________________________________________*/
class LimGraphFitplot extends LimGraphControls {

   #axis_x
   #axis_y

   #error_bar_button_visible
   #legend_button_visible

   #error_bars_visible
   #error_bars_visible_changed
   #legend_visible
   #legend_visible_changed

   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/doseResponse_common.svg";
   static name = "Fitplot";

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.#axis_x = new LimGraphAxis(pars?.axis_x ?? null);
      this.#axis_y = new LimGraphAxis(pars?.axis_y ?? null);

      this.#error_bar_button_visible = pars?.error_bar_button_visible ?? true;
      this.#legend_button_visible = pars?.legend_button_visible ?? true

      this.onRowSelectionChanged = that => { this.setSelection(); };

      if (!this.iconres)
         this.iconres = LimGraphFitplot.iconres;
      if (!this.name)
         this.name = LimGraphFitplot.name;

      this.#error_bars_visible = true;
      this.#error_bars_visible_changed = new LimSignal([]);
      if (this.#error_bar_button_visible) {
         this.optionList.set("errorBarsVisible", {
            type: "option",
            title: "Error bars visible",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_show_int.svg"
         });
      }

      this.#legend_visible = this.legendVisible;
      this.#legend_visible_changed = new LimSignal([]);
      if (this.#legend_visible && this.#legend_button_visible) {
         if (true) {
            this.optionList.set("legendVisible", {
               type: "option",
               title: "Legend visible",
               iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_legend.svg"
            });
         }
      }

      this.update();
   }

   // error_bars_visible
   get errorBarsVisibleOptionValue() {
      return this.#error_bars_visible;
   }
   set errorBarsVisibleOptionValue(val) {
      if (this.#error_bars_visible === val)
         return;
      this.#error_bars_visible = val;
      this.#error_bars_visible_changed.emit(this);
      this.update();
   }
   get errorBarsVisibleValueChanged() {
      return this.#error_bars_visible_changed;
   }

   // legend_visible
   get legendVisibleOptionValue() {
      return this.#legend_visible;
   }
   set legendVisibleOptionValue(val) {
      if (this.#legend_visible === val)
         return;
      this.#legend_visible = val;
      this.#legend_visible_changed.emit(this);
      this.update();
   }
   get legendVisibleValueChanged() {
      return this.#legend_visible_changed;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setData() {

   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setSelection() {

   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   update() {
      //TODO check if new or old and something has changed
      this.fetchAndFillGraphDataSource();

      this.updateFeatureList();
      this.updateGraph();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   updateGraph() {
      if (!this.tableData)
         return;

      this.setData();
      this.drawFitplot();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async drawFitplot() {
      if (!this.tableData || !this.containerGraph)
         return;

      const td = this.tableData;

      const { color_base, color_text, color_midlight, color_mid, color_graph_data, color_graph_highlight } = this.styles();
      const { color_tooltip, color_tooltiptext } = this.styles_tooltip();
      const palette = Bokeh.Palettes.Category10_10;

      let fit_id = this.fitColId;
      let fit_meta;
      let x_id, y_id, error_id;
      if (fit_id) {
         const meta = td.colMetadata(fit_id);
         if (td.colIdList.includes(meta?.xColId ?? ""))
            x_id = meta.xColId;
         if (td.colIdList.includes(meta?.yColId ?? ""))
            y_id = meta.yColId;
         fit_meta = meta.fit;
      }

      let legend_label = 'Data';
      if (y_id) {
         let err_idx = [
            td.colMetadataList.findIndex(meta => meta?.aggregated?.startsWith?.("StErr") && meta?.duplicatedFrom === y_id),
            td.colMetadataList.findIndex(meta => meta?.aggregated?.startsWith?.("StDev") && meta?.duplicatedFrom === y_id)
         ].find(item => 0 <= item);

         if (0 <= err_idx) {
            error_id = td.colIdAt(err_idx);
            legend_label = `Data with S${td.colMetadataAt(err_idx).aggregated[2]}`; //`
         }
      }

      let graph_limits = { l: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: Number.MAX_VALUE, t: -Number.MAX_VALUE };

      const fit_has_data = 0 < td.colData(fit_id).filter(item => !!item).length;
      if (!fit_id || !fit_has_data || !x_id || !y_id)
         return;

      if (!this.#axis_x.range_scale || this.#axis_x.range_scale === "auto") {
      if (fit_meta.name === "Dose response")
         this.#axis_x.range_scale = "log";
         else
            this.#axis_x.range_scale = "linear";
      }
      const is_x_axis_log = this.#axis_x.range_scale === "log";
      const is_y_axis_log = this.#axis_y.range_scale === "log";

      let graph_data_source;

      const groups = td.groups;
      graph_data_source = groups.map(() => ({
         raw: new Bokeh.ColumnDataSource(),
         fit: new Bokeh.ColumnDataSource(),
         pts: new Bokeh.ColumnDataSource()
      }));

      const group_name = (i) => {
         return td.groupedBy.map(id => {
            const j = td.colIndexById(id);
            return td.dataColumnList[j][i]
         }).filter(item => item !== null).map(item => `${item}`).join(", "); //`
      };

      let series_names = groups.length ? groups.map((rows, index) => group_name(rows[0])) : [""];
      let series_errors = groups.length ? groups.map(() => "") : [""]
      let series_color_data = 1 < groups.length ? groups.map((_, index) => palette[index % groups.length]) : [color_graph_data];
      let series_color_fit = 1 < groups.length ? groups.map((_, index) => palette[index % groups.length]) : [color_graph_highlight];
      let series_raw_legend_labels = groups.length ? series_names.map(item => `${item}: ${legend_label}`) : [legend_label]; //`

      let x_meta = this.axisMetadata({
         col_ids: x_id,
         axis_settings: this.#axis_x,
         autoscale: this.autoScaleOptionValue,
      });
      let y_meta = this.axisMetadata({
         col_ids: y_id,
         axis_settings: this.#axis_y,
         autoscale: this.autoScaleOptionValue,
      });

      const title_x = x_meta.meta.title ?? "";
      const title_y = y_meta.meta.title ?? "";
      const units_x = x_meta.meta.units ?? "";
      const units_y = y_meta.meta.units ?? "";

      const xAxisPrintFn = LimTableData.makePrintFunction(x_meta.meta);
      const yAxisPrintFn = LimTableData.makePrintFunction(y_meta.meta);

      for (let g = 0; g < groups.length; g++) {
         const serie_name = series_names[g];
         const serie_color_data = Number.isInteger(series_color_data[g]) ? integerToRGB(series_color_data[g]) : series_color_data[g];
         const serie_color_fit = Number.isInteger(series_color_fit[g]) ? integerToRGB(series_color_fit[g]) : series_color_fit[g];

         const fit_idx = td.colIndexById(fit_id);
         const fdata = td.dataColumnList[fit_idx][groups[g][0]];
         const xdata = td.colData(x_id, groups[g]);
         const ydata = td.colData(y_id, groups[g]);
         let data_filter = is_x_axis_log ? xdata.map(item => item !== null && 0 < item) : xdata.map(item => item !== null);
         data_filter = is_y_axis_log ? ydata.map((item, i) => data_filter[i] && item !== null && 0 < item) : ydata.map((item, i) => data_filter[i] && item !== null);
         const data = {
            x: xdata.filter((item, index) => data_filter[index]),
            y: ydata.filter((item, index) => data_filter[index]),
            r: data_filter.map((val, index) => val ? index : -1).filter(item => 0 <= item).map(item => groups[g][item])
         };
         data.color = data.x.map(() => serie_color_data);
         data.serie = data.x.map(() => series_names[g]);

         const xlo = Math.min(...data.x);
         const xhi = Math.max(...data.x);
         graph_limits.l = Math.min(graph_limits.l, xlo);
         graph_limits.r = Math.max(graph_limits.r, xhi);
         graph_limits.b = Math.min(graph_limits.b, Math.min(...data.y));
         graph_limits.t = Math.max(graph_limits.t, Math.max(...data.y));

         if (error_id) {
            const error_data = this.tableData.colData(error_id).filter((item, index) => data_filter[index]);
            data.err = error_data;
            data.errlo = data.y.map((item, index) => item - error_data[index]);
            data.errhi = data.y.map((item, index) => item + error_data[index]);
            graph_limits.b = Math.min(graph_limits.b, ...data.errlo.filter(item => isFinite(item)));
            graph_limits.t = Math.max(graph_limits.t, ...data.errhi.filter(item => isFinite(item)));
         }

         const tooltips = [];
         for (let i = 0; i < data.x.length; i++) {
            tooltips.push(`
               <div>
               <div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 5px; margin: -6px -6px -15px -6px;">
               <div style="font-size:11px;/*margin:9px;*/">
                  <span><svg width="20" height="12"><circle cx="6" cy="6" r="5" fill="${serie_color_data}" stroke="${color_text}" /></svg></span>
                  <span>${serie_name === "" ? "Data" : serie_name}</span>
               </div>
               <table style="font-size:11px;margin:9px; ">
                  <tr>
                     <td style="padding:2px 5px;">${title_x}</td>
                     <td style="padding:2px 1px 2px 5px;text-align:right;">${xAxisPrintFn(data.x[i])}</td>
                     <td style="padding:2px 5px 2px 1px;">${units_x}</td>
                  </tr>
                  <tr>
                     <td style="padding:2px 5px;">${title_y}</td>
                     <td style="padding:2px 1px 2px 5px;text-align:right;">${yAxisPrintFn(data.y[i])}</td>
                     <td style="padding:2px 5px 2px 1px;">${units_y}</td>
                  </tr>
                  <tr>
                     <td style="padding:2px 5px;">Std. error</td>
                     <td style="padding:2px 1px 2px 5px;text-align:right;">${yAxisPrintFn(data?.err?.[i] ?? "n/a")}</td>
                     <td style="padding:2px 5px 2px 1px;">${units_y}</td>
                  </tr>
               </table>
            </div>
            </div>`);
         }
         data.tooltip = tooltips;

         graph_data_source[g].raw.data = data;

         let fit_object = null;
         try { fit_object = JSON.parse(fdata); }
         catch { }

         if (fit_meta && fit_object?.data) {
            let fitMetaFn;
            eval(`fitMetaFn = ${fit_meta.fnJsExpr}`);//`

            let params = [...fit_object?.data.paramValues];
            const is_valid = params.map(item => item == null ? 0 : 1).reduce((previous, current) => previous += current, 0);
            const has_error_msg = fit_object?.data?.fitErrorMessage ?? null;
            if(has_error_msg != null)
               series_errors[g] = fit_object?.data?.fitErrorMessage;


            let fn = function(params) { return NaN; }
            let fitx = [];
            let fity = [];

            if (is_valid) {
               fn = fitMetaFn(...params)
               params[0] = params[0] ?? Infinity;
               if (is_x_axis_log) {
                  let iter = 0;
                  const log10_xlo = Math.log10(xlo / 1000);
                  const log10_xhi = Math.log10(xhi * 1000);
                  const log10_step = (log10_xhi - log10_xlo) / 1000;
                  for (let log10_x = log10_xlo; log10_x < log10_xhi && iter < 1000; log10_x += log10_step, iter++) {
                     const x = Math.pow(10.0, log10_x);
                     fitx.push(x)
                     fity.push(fn(x));
                  }
                  graph_limits.l = Math.min(graph_limits.l, xlo / 10);
                  graph_limits.r = Math.max(graph_limits.r, xhi * 10);
               }
               else {
                  const tRange = 0.1 * (xhi - xlo);
                  const hRange = 0.01 * (xhi - xlo);
                  for (let x = xlo - 3 * tRange; x < xhi + 3 * tRange; x += hRange) {
                     fitx.push(x)
                     fity.push(fn(x));
                  }
                  graph_limits.l = Math.min(graph_limits.l, xlo - tRange);
                  graph_limits.r = Math.max(graph_limits.r, xhi + tRange);
               }
            }

            const fmt = (number, digits = 3) => {
               return typeof number !== "number" || isNaN(number) ? "n/a" :
                  number < -10e21 ? "-inf" :
                     10e21 < number ? "+inf" :
                        number.toFixed(digits);
            }

            const rowN = [];
            const rowH = fit_meta.paramNames.map(item => `<td style="padding:2px 5px;text-align: right;">${item}</td>`).join("");
            const row0 = fit_object.data.paramValues.map(item => item ? item.toFixed(3) : "n/a").map(item => `<td style="padding:2px 5px;text-align: right;">${item}</td>`).join("");
            for (let i = 0; i < fit_meta.paramErrorNames.length; i++) {
               const row = fit_object.data.paramErrorValues.map(column => column[i]).map(item => fmt(item, 3)).map(item => `<td style="padding:2px 5px;text-align: right;">${item}</td>`).join("");
               rowN.push(`<tr><td style="padding:2px 5px;">${fit_meta.paramErrorNames[i]}</td>${row}</tr>`);
            }

            const goodness = [];
            for (let i = 0; i < fit_meta.fitGoodnessNames.length; i++) {
               goodness.push(`
                  <tr>
                     <td style="padding:2px 5px;">${fit_meta.fitGoodnessNames[i]}</td>
                     <td style="padding:2px 5px;text-align:right;">${fmt(fit_object.data?.fitGoodnessValues?.[i], 4)}</td>
                  </tr>`);
            }

            const tooltip = `
         <div>
         <div style="background-color: ${color_tooltip}; color: ${color_tooltiptext}; padding: 5px; margin: -6px -6px -15px -6px;">
         <div style="font-size:11px;margin:9px;"><span><svg width="20" height="8"><line x1="0" y1="4" x2="15" y2="4" style="stroke:${serie_color_fit};stroke-width:3" /></svg></span><span>${serie_name === "" ? "Fit" : serie_name}</span></div>
            <table style="font-size:11px;margin:9px;">
               <tr style="border-bottom: 1px solid ${color_mid};"><td style="padding:2px 5px;">Fit parameters</td>${rowH}</tr>
               <tr><td style="padding:2px 5px;">Best-fit value</td>${row0}</tr>${rowN.join("\n")}
            </table>
         <table style="font-size:11px;margin:9px;">${goodness.join("\n")}</table>
         </div></div>`;

            graph_data_source[g].fit.data = {
               x: fitx,
               y: fity,
               color: fitx.map(() => serie_color_fit),
               serie: fitx.map(() => serie_name),
               tooltip: fitx.map(() => tooltip)
            };
            const valid_fity = fity.filter(value => isFinite(value));
            graph_limits.b = Math.min(graph_limits.b, ...valid_fity);
            graph_limits.t = Math.max(graph_limits.t, ...valid_fity);

            if (fit_object?.data?.points?.length) {
               let [ptx, pty, names] = [[], [], []];
               for (let pt of fit_object.data.points) {
                  if (pt?.x) {
                     ptx.push(pt.x);
                     pty.push(pt.y ?? fn(pt.x));
                     names.push(pt.name ?? "");
                  }
               }
               graph_data_source[g].pts.data = { x: ptx, y: pty, name: names, color: ptx.map(() => serie_color_fit), serie: ptx.map(() => serie_name) };
            }
            else
               graph_data_source[g].pts.data = { x: [], y: [], name: [], color: [], serie: [] };
         }
      }

      const fig = Bokeh.Plotting.figure({
         title: this.title,
         x_range: this.#axis_x.createRange(graph_limits.l, graph_limits.r, true),
         x_scale: this.#axis_x.createScale(),
         y_range: this.#axis_y.createRange(graph_limits.b, graph_limits.t, true),
         y_scale: this.#axis_y.createScale(),
         tools: this.graphTools.filter(item => item !== "hover").join(","),
         sizing_mode: "stretch_both",
      });

      let renderers_all = [];
      let renderers_data = [];
      let legend_items = [];
      for (let i = 0; i < graph_data_source.length; i++) {
         const dataSource = graph_data_source[i];
         const dataRenderer = fig.circle({ field: "x" }, { field: "y" }, {
            source: dataSource.raw, size: 8,
            fill_color: { field: "color" }, fill_alpha: 1.0, line_color: color_text, line_alpha: 0.7,
            selection_fill_color: { field: "color" }, selection_fill_alpha: 1.0, selection_line_color: color_text, selection_line_alpha: 0.7,
            nonselection_fill_color: { field: "color" }, nonselection_fill_alpha: 1.0, nonselection_line_color: color_text, nonselection_line_alpha: 0.7,
         });
         renderers_all.push(dataRenderer);
         renderers_data.push(dataRenderer);

         legend_items.push(new Bokeh.LegendItem({
            label: series_raw_legend_labels[i],
            renderers: [dataRenderer],
            index: 0
         }));

         const fitRenderer = fig.line({ field: "x" }, { field: "y" }, {
            source: dataSource.fit,
            line_width: 2, color: series_color_fit[i],
         });
         renderers_all.push(fitRenderer);

         if (error_id && this.#error_bars_visible) {
            const tee_head_color = dataSource.raw.data.color?.[0]
            let whisker_head = new Bokeh.TeeHead({
               line_color: tee_head_color,
               line_alpha: 0.7,
               line_width: 2,
               size: 10
            });
            let whisker = new Bokeh.Whisker({
               base: { field: 'x' },
               lower: { field: 'errlo' },
               upper: { field: 'errhi' },
               source: dataSource.raw,
               line_color: { field: "color" },
               line_alpha: 0.7,
               line_width: 2,
               lower_head: whisker_head,
               upper_head: whisker_head
            });
            fig.add_layout(whisker);
         }

         legend_items.push(new Bokeh.LegendItem({
            label: (series_names[i] ? `${series_names[i]}: ` : "") + (series_errors[i]?.length ? `${series_errors[i]}` : "Fit"), //`
            renderers: [fitRenderer],
            index: dataSource?.fit?.data?.x?.length ? 0 : null
         }));

         fig.circle({ field: "x" }, { field: "y" }, {
            source: dataSource.pts, size: 8, fill_color: { field: "color" }, line_color: { field: "color" }
         });
         fig.add_layout(new Bokeh.LabelSet({
            x: { field: "x" }, y: { field: "y" }, text: { field: "name" }, x_offset: 0, y_offset: 12, source: dataSource.pts,
            text_font: this.fontFamily, text_font_size: "12px", text_align: "center", text_color: { field: "color" },
            background_fill_color: color_base, background_fill_alpha: 0.8
         }));
      }

      if (this.#legend_visible) {
         if (this.legendOutsidePlacement) {
            const max_label_width = Math.max(0, ...series_raw_legend_labels.map(value => this.measureLegendLabelWidth(value))) + 10;
            let legend = new Bokeh.Legend({
               items: legend_items,
               location: 'center', orientation: this.legendOrientation,
               label_width: max_label_width
            });
            this.styleGraphLegend(legend);
            fig.add_layout(legend, this.legendVisibility);

            fig.js_property_callbacks = {
               "change:inner_width": [new Bokeh.CustomJS({
                  args: {
                     legend: legend,
                     fig: fig,
                     max_label_width: max_label_width,
                     fn: (legend, fig, max_label_width) => {
                        legend.ncols = Math.max(3, Math.floor((fig.inner_width ?? 1)
                           / (max_label_width + legend.glyph_width + legend.label_standoff + legend.padding)));
                        legend.change.emit();
                     }
                  },
                  code: "fn(legend, fig, max_label_width)"
               })]
            };
         }
         else if (this.legendVisible) {
            fig.legend.location = this.legendLocation;
            fig.legend.items = legend_items;
         }
      }

      if (this.graphTools.includes("hover")) {
         let hover_tool = new Bokeh.HoverTool({
            renderers: renderers_all,
            tooltips: `@tooltip`
         });
         fig.add_tools(hover_tool);
      }
      if (this.graphTools.includes("tap")) {
         let tap_tool = new Bokeh.TapTool({ renderers: this.dataRenderers, behavior: 'inspect' });
         tap_tool.callback = new Bokeh.CustomJS({
            args: {
               fn: (cb_obj, cb_data) => {
                  const sel = cb_data?.source?.inspected?.indices;
                  const data = cb_data?.source?.inspect?.sender?.data;
                  if (this.rowSelection && Array.isArray(sel) && Array.isArray(data?.r)) {
                     this.rowSelection = [data.r[sel[0]]];
                  }
               }
            },
            code: "fn(cb_obj, cb_data);"
         });
         fig.add_tools(tap_tool);
      }

      this.styleGraphFigure(fig, x_meta.title, y_meta.title);
      this.#axis_x.formatAxis(fig.xaxis);
      this.#axis_x.formatGrid(fig.xgrid);
      this.#axis_y.formatAxis(fig.yaxis);
      this.#axis_y.formatGrid(fig.ygrid);

      this.containerGraph.innerText = '';
      this.createCustomToolBar(fig);
      let handle = new Bokeh.Row({ children: [fig], sizing_mode: "stretch_both" });
      Bokeh.Plotting.show(handle, this.containerGraph);
   }
}

/*___________________________________________________________________________*/
class LimGraphHistogramV2 extends LimGraphControls {

   #bin_count
   #bin_value_min
   #bin_value_max
   #bin_values

   #graphDataSources
   #data_is_cardinal
   #data_label
   #data_fit_enabled
   #data_xmin
   #data_xmax

   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/histo_common.svg";
   static name = "Histogram";

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   constructor(pars, ...args) {
      super(pars, ...args);

      this.#bin_count = Math.round(LimGraphBokeh.tryNumber(pars?.bin_count) ?? 20);
      this.#bin_value_min = LimGraphBokeh.tryNumber(pars?.bin_value_min);
      this.#bin_value_max = LimGraphBokeh.tryNumber(pars?.bin_value_max);
      this.#bin_values = LimGraphBokeh.tryArray(pars?.bin_values) ?? LimGraphBokeh.tryParseArray(pars?.bin_values);

      this.onRowSelectionChanged = that => { this.setSelection(); };

      if (!this.iconres)
         this.iconres = LimGraphHistogramV2.iconres;
      if (!this.name)
         this.name = LimGraphHistogramV2.name;

      this.update();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setData() {
      if (!this.tableData || !this.container)
         return;

      const histo_id = this.histoColId;

      const bar_width = 0.9;
      const { color_graph_default, color_graph_selection, color_graph_nonselection } = this.selection_styles();

      this.#data_label = "";
      this.#data_fit_enabled = false;

      let fit = { x: [], y: [] };
      let data = {};

      const histo_idx = this.tableData.colIndexById(histo_id);
      if (histo_idx < 0)
         return;

      this.#data_is_cardinal = this.tableData.colIsNumericAt(histo_idx);
      if (0 <= histo_idx && this.#data_is_cardinal) {
         const xmeta = this.tableData.colMetadataAt(histo_idx);
         let xmin = this.#bin_value_min ?? xmeta?.globalRange?.min;
         let xmax = this.#bin_value_max ?? xmeta?.globalRange?.max;
         data = this.tableData.columnHistogramEquidistantBinsAt(
            histo_idx,
            this.#bin_count,
            this.autoScaleOptionValue ? undefined : xmin,
            this.autoScaleOptionValue ? undefined : xmax,
            this.rowSelection,
            this.cumulativeOptionValue,
            this.normalizedOptionValue)

         const N = data?.cnt?.length ?? 0;
         data.fill_alpha = new Array(N).fill(this.rowSelection.length ? 0.3 : 0.8);
         data.fill_color = new Array(N).fill(this.rowSelection.length ? color_graph_nonselection : color_graph_default);
         if (0 < N) {
            this.#data_fit_enabled = true;
            [this.#data_xmin, this.#data_xmax] = [data.los[0], data.his[data.his.length - 1]];
            if (this.fitNormalOptionValue && (0 < data?.cnt?.length ?? 0)) {

               const fitMetaPDFn = (mean, stdev, scale) => {
                  const term1 = scale / (stdev * Math.sqrt(2 * Math.PI));
                  return x => term1 * Math.exp(-0.5 * ((x - mean) / stdev) ** 2);
               };
               const fitMetaCDFn = (mean, sigma, scale) => {
                  return x => {
                     let z = (x - mean) / Math.sqrt(2 * sigma * sigma);
                     let t = 1 / (1 + 0.3275911 * Math.abs(z));
                     let a1 = 0.254829592;
                     let a2 = -0.284496736;
                     let a3 = 1.421413741;
                     let a4 = -1.453152027;
                     let a5 = 1.061405429;
                     let erf = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
                     let sign = 1;
                     if (z < 0) {
                        sign = -1;
                     }
                     return scale * ((1 / 2) * (1 + sign * erf));
                  }
               }

               let scale = 0;
               if(!this.cumulativeOptionValue)
               for (let i = 0; i < data.cnt.length; i++) {
                  scale += data.cnt[i] * (data.his[i] - data.los[i]);
               }
               else if(data.cnt.length)
                  scale = data.cnt[data.cnt.length - 1];

               const [mean, stdev] = this.tableData.colDataStatsAt(histo_idx, ["mean", "stdev"]);
               this.#data_label = `\u03bc=${mean.toFixed(3)}, \u03c3=${stdev.toFixed(4)}`
               const fn = !this.cumulativeOptionValue ? fitMetaPDFn(mean, stdev, scale) : fitMetaCDFn(mean, stdev, scale);
               const xrng = this.#data_xmax - this.#data_xmin;
               const step = (1.4 * xrng) / 1000;
               for (let x_ = this.#data_xmin - 0.2 * xrng; x_ <= this.#data_xmax + 0.2 * xrng; x_ += step) {
                  fit.x.push(x_)
                  fit.y.push(fn(x_));
               }
            }
            else {
               this.#data_label = "";
            }

            const d = (1 - bar_width) * (this.#data_xmax - this.#data_xmin) / N / 2;
            data.los = data.los.map(x => x + d);
            data.his = data.his.map(x => x - d);

         }
      }
      else if (0 <= histo_idx) {
         data = this.tableData.columnHistogramEnumerationAt(histo_idx, this.autoScaleOptionValue ? undefined : this.#bin_values, this.rowSelection);
         const N = data?.cnt?.length ?? 0;
         data.fill_alpha = new Array(N).fill(this.rowSelection.length ? 0.3 : 0.8);
         data.fill_color = new Array(N).fill(this.rowSelection.length ? color_graph_nonselection : color_graph_default);
         this.#data_label = "";
      }

      this.#graphDataSources = {
         histo: new Bokeh.ColumnDataSource({ data: data }),
         fit: new Bokeh.ColumnDataSource({ data: fit })
      };
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   setSelection() {
      const { color_graph_default, color_graph_selection, color_graph_nonselection } = this.selection_styles();

      let data = {};
      const histo_idx = this.tableData.colIndexById(this.histoColId);
      if (histo_idx < 0)
         return;

      if (0 <= histo_idx && this.#data_is_cardinal) {
         const xmeta = this.tableData.colMetadataAt(histo_idx);
         let xmin = this.#bin_value_min ?? xmeta?.globalRange?.min;
         let xmax = this.#bin_value_max ?? xmeta?.globalRange?.max;
         data = this.tableData.columnHistogramEquidistantBinsAt(
            histo_idx,
            this.#bin_count,
            this.autoScaleOptionValue ? undefined : xmin,
            this.autoScaleOptionValue ? undefined : xmax,
            this.rowSelection,
            this.cumulativeOptionValue,
            this.normalizedOptionValue)

         const N = data?.cnt?.length ?? 0;
         data.fill_alpha = new Array(N).fill(this.rowSelection.length ? 0.3 : 0.8);
         data.fill_color = new Array(N).fill(this.rowSelection.length ? color_graph_nonselection : color_graph_default);
      }
      else if (0 <= histo_idx) {
         data = this.tableData.columnHistogramEnumerationAt(histo_idx, this.autoScaleOptionValue ? undefined : this.#bin_values, this.rowSelection);
         const N = data?.cnt?.length ?? 0;
         data.fill_alpha = new Array(N).fill(this.rowSelection.length ? 0.3 : 0.8);
         data.fill_color = new Array(N).fill(this.rowSelection.length ? color_graph_nonselection : color_graph_default);
      }

      this.#graphDataSources.histo.data = data;
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   update() {
      //TODO check if new or old and something has changed
      this.fetchAndFillGraphDataSource();

      this.updateFeatureList();
      this.updateGraph();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   updateGraph() {
      if (!this.tableData)
         return;

      this.setData();
      this.drawHistogram();
   }

   //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   async drawHistogram() {
      this.containerMessage = "";
      if (!this.tableData || !this.container)
         return;

      const histoIdx = this.tableData.colIndexById(this.histoColId);
      if(histoIdx < 0) {
         this.containerMessage = "Missing data column";
         return;
      }

      const bar_width = 0.9;
      const { color_graph_default, color_graph_selection, color_graph_nonselection } = this.selection_styles();


      let fig = null;
      let renderer = null;

      if (this.#data_is_cardinal) {
         const xrng = new Bokeh.Range1d({ reset_start: this.#data_xmin, reset_end: this.#data_xmax, start: this.#data_xmin, end: this.#data_xmax });
         const yrng = new Bokeh.DataRange1d({ start: 0 });

         fig = Bokeh.Plotting.figure({
            title: this.title, x_range: xrng, y_range: yrng,
            tools: this.graphTools.filter(item => item !== "tap").join(","),
            sizing_mode: "stretch_both"
         });

         renderer = fig.quad({
            left: { field: "los" }, right: { field: "his" }, top: { field: "cnt" }, bottom: { field: "sel" },
            source: this.#graphDataSources.histo,
            fill_color: { field: "fill_color" }, fill_alpha: { field: "fill_alpha" }, line_color: null
         });

         fig.quad({
            left: { field: "los" }, right: { field: "his" }, top: { field: "sel" }, bottom: 0,
            source: this.#graphDataSources.histo,
            fill_color: color_graph_selection, fill_alpha: 1.0, line_color: null
         });

         fig.line({ field: 'x' }, { field: 'y' }, {
            line_width: 2,
            source: this.#graphDataSources.fit,
            color: Bokeh.Palettes.Category10_3[1],
            legend_label: this.#data_label
         });

      }
      else {
         const xrng = new Bokeh.FactorRange({ factors: this.#graphDataSources.histo.data.labels });
         const yrng = new Bokeh.DataRange1d({ start: 0 });
         fig = Bokeh.Plotting.figure({
            title: "Histogram", x_range: xrng, y_range: yrng,
            tools: this.graphTools.filter(item => item !== "tap").join(","),
            sizing_mode: "stretch_both"
         });

         this.styleGraphFigure(fig, this.tableData.colTitleAndUnitAt(histoIdx), "Count");

         renderer = fig.vbar({
            x: { field: "labels" }, bottom: { field: "sel" }, top: { field: "cnt" }, width: bar_width,
            source: this.#graphDataSources.histo,
            fill_color: { field: "fill_color" }, fill_alpha: { field: "fill_alpha" }, line_color: null
         });

         fig.vbar({
            x: { field: "labels" }, bottom: 0, top: { field: "sel" }, width: bar_width,
            source: this.#graphDataSources.histo,
            fill_color: color_graph_selection, fill_alpha: 0.8, line_color: null
         });
      }

      if (this.graphTools.includes("tap")) {
         const tap = new Bokeh.TapTool({ renderers: [renderer], behavior: 'inspect' });
         tap.callback = new Bokeh.CustomJS({
            args: {
               fn: (cb_obj, cb_data) => {
                  const sel = cb_data?.source?.inspected?.indices;
                  if (this.rowSelection && Array.isArray(sel)) {
                     let s = sel.map(item => this.#graphDataSources.histo.data.rows[item]).reduce((prev, curr) => prev.concat(curr), [])
                     s.sort();
                     this.rowSelection = s;
                  }
               }
            },
            code: "fn(cb_obj, cb_data);"
         });
         fig.add_tools(tap);

         let obj = this;
         document.addEventListener("keyup", function (event) {
            if (event.key !== 'Escape')
               return;
            if (!tap || !tap.active)
               return;
            obj.rowSelection = [];
         });
      }

      fig.legend.visible = !!this.#data_label;


      this.fitNormalOptionEnabled = this.#data_fit_enabled;
      this.autoScaleOptionEnabled = this.#data_is_cardinal;

      this.styleGraphFigure(fig, this.tableData.colTitleAndUnitAt(histoIdx), "Count");
      //this.#axis_x.formatAxis(fig.xaxis);
      //this.#axis_x.formatGrid(fig.xgrid);
      //this.#axis_y.formatAxis(fig.yaxis);
      //this.#axis_y.formatGrid(fig.ygrid);

      this.containerGraph.innerText = '';
      this.createCustomToolBar(fig);
      let handle = new Bokeh.Row({ children: [fig], sizing_mode: "stretch_both" });
      Bokeh.Plotting.show(handle, this.containerGraph);
   }
}

class LimGraphContainerV2 extends HTMLElement {
   #bokehInstance
   #args

   constructor() {
      super();
   }

   initialize(...args) {
      this.className = "lim-container";
      this.style.display = "flex";
      this.style.flex = "1 1 100%";
      this.#args = args;
   }

   connectedCallback() {
      const [bokehClass, ...remainingArgs] = this.#args;
      this.#bokehInstance = new bokehClass(...remainingArgs);
      this.appendChild(this.#bokehInstance.containerGraph);
      this.appendChild(this.#bokehInstance.containerToolbar);
      this.#bokehInstance.connectToDocument();
      this.#bokehInstance.fetchAndUpdateTable();

      const defPropRO = (name) => {
         if (!Object.hasOwn(this, name))
            Object.defineProperty(this, name, {
               get() { return this.#bokehInstance[name]; },
               enumerable: true,
               configurable: false
            });
      };

      const defPropRW = (name) => {
         if (!Object.hasOwn(this, name))
            Object.defineProperty(this, name, {
               get() { return this.#bokehInstance[name]; },
               set(newValue) { this.#bokehInstance[name] = newValue; },
               enumerable: true,
               configurable: false
            });
      };

      this.optionList = this.#bokehInstance.optionList;
      for (let opt of this.optionList) {
         if (opt[1].type === "selection" || opt[1].type === "multi-selection") {
            defPropRO(`${opt[0]}OptionValues`);
            defPropRO(`${opt[0]}OptionValuesChanged`);
         }
         defPropRW(`${opt[0]}OptionValue`);
         defPropRO(`${opt[0]}OptionValueChanged`);

         defPropRW(`${opt[0]}OptionEnabled`);
         defPropRO(`${opt[0]}OptionEnabledChanged`);
      }
   }

   disconnectedCallback() {
      this.#bokehInstance.disconnectFromDocument();
      this.innerHTML = "";
      this.optionList = null;
      this.#bokehInstance = null;
   }

   get title() {
      return this.#args?.[1]?.title ?? "";
   }

   get name() {
      return this.#args?.[1]?.name || this.#args?.[0].name;
   }

   get iconres() {
      return this.#args?.[1]?.title || this.#args?.[0].iconres;
   }
};

customElements.define('lim-graph-container-v2', LimGraphContainerV2);
const createGraphContainerV2 = (bokehClass, ...pars) => {
   const el = new LimGraphContainerV2();
   el.initialize(bokehClass, ...pars);
   return el;
}

LimClassFactory.registerConstructor("LimGraphLinechartV2", (...pars) => createGraphContainerV2(LimGraphLinechartV2, ...pars));
LimClassFactory.registerConstructor("LimGraphScatterplotV2", (...pars) => createGraphContainerV2(LimGraphScatterplotV2, ...pars));
LimClassFactory.registerConstructor("LimGraphHeatmap", (...pars) => createGraphContainerV2(LimGraphHeatmap, ...pars));
LimClassFactory.registerConstructor("LimGraphBarchartV2", (...pars) => createGraphContainerV2(LimGraphBarchartV2, ...pars));
LimClassFactory.registerConstructor("LimGraphFitplot", (...pars) => createGraphContainerV2(LimGraphFitplot, ...pars));
LimClassFactory.registerConstructor("LimGraphHistogramV2", (...pars) => createGraphContainerV2(LimGraphHistogramV2, ...pars));