Bokeh.logger.set_level("error");

/*___________________________________________________________________________*/
function integerToRGBA(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(",") + ")";
}

/*___________________________________________________________________________*/
class LimGraphBokeh extends LimTableDataClient {
   #tableRowVisibility
   #tableRowSelection
   #rowSelection
   #blockSetRowSelection
   #blockUpdateRowSelection

   #graphTools

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

   #error_message

   constructor(container, ...args) {
      super(...args)

      const [pars, ...remainingArgs] = args;

      this.#tableRowVisibility = pars?.tableRowVisibility ?? "all"
      this.#tableRowSelection = pars?.dataSelectionEnabled ?? true;
      this.#rowSelection = [];
      this.#blockSetRowSelection = false;
      this.#blockUpdateRowSelection = false;

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

      this.#graphTools = LimGraphBokeh.tryArray(pars?.graphTools) ?? LimGraphBokeh.tryParseArray(pars?.graphTools);
      this.#graphTools = this.#graphTools.filter(item => item != "save")

      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 ?? "";

      this.#error_message = "Graph is not available"

      this.optionList = new Map();
      if (0 < (pars?.tableRowSelectVisibility?.length ?? 0)) {
         this.optionList.set("selectedRowVisibility", {
            type: "selection",
            title: "Show data for"/*,
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/autosize_columns.svg"
            */
         });
      }
   }

   onTableChanged() {
      this.fetchAndUpdateTable();
   }

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

   async fetchAndFillGraphDataSource() {
      const cols = [... new Set(this.tableData.systemColIdList.concat(this?.dataColumns ?? [])).values()];
      const rowFilterList = this.makeDefaultRowFilterList().concat(this?.rowFilter ?? []);
      let pars = {
         cols: JSON.stringify(cols),
         filter: JSON.stringify({ op: "all", filters: rowFilterList })
      };
      await this.fetchTableData(pars, () => {
         this?.fillGraphDataSource?.();
         this?.createGraphFigure?.();
      });
   }


   onCurrentLoopIndexesChanged(val, change) {
      if (this.isSelectedRowVisiblityIncludingAnyOfLoops(change))
         this.fetchAndUpdateTable();
      else if (this.#tableRowSelection && this?.tableData?.isFrameTable && val) {
         const rowIndex = this.tableData.loopIndexesToRowIndex(val);
         if (0 <= rowIndex) {
            this.#blockUpdateRowSelection = true;
            this.rowSelection = [rowIndex];
            this.#blockUpdateRowSelection = false;
         }
      }
   }

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

   onSelectedRowVisibilityChanged(val, oldVal) {
      this.fetchAndFillGraphDataSource();
   }

   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.#blockUpdateRowSelection) {
         if (this.#tableRowSelection && this?.tableData?.isObjectTable) {
            this.currentObjectSelection = this.tableData.rowIndexesToObjectSelection(this.#rowSelection);
         }
         else if (this.#tableRowSelection && this?.tableData?.isFrameTable && this.#tableRowVisibility === "all") {
            this.currentLoopIndexes = this.tableData.rowIndexToLoopIndexes(this.#rowSelection);
         }
      }
   }

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

   get graphTools() {
      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";
   }

   set error_message(value) {
      this.#error_message = value;
   }
   get error_message() {
      return  this.#error_message;
   }

   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_graph_data: rs.getPropertyValue("--color-graph-data"),
         color_graph_highlight: rs.getPropertyValue("--color-graph-highlight")
      };
   };

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

   scheme() {
      const r = document.querySelector(":root");
      var rs = getComputedStyle(r);
      return rs.getPropertyValue("--color-scheme");
   }
   isLightColorScheme() {
      return this.scheme().includes('light');
   }
   isDarkColorScheme() {
      return this.scheme().includes('dark');
   }

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

   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;
   }

   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;
   }

   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;
   }

   colorList(name) {
      return LimGraphBokeh.tableauColorList();
   }

   styleNoDataFigure(fig) {
      this.styleGraphFigure(fig);

      fig.xgrid.grid_line_color = null;
      fig.xgrid.minor_grid_line_color = null;
      fig.ygrid.grid_line_color = null;
      fig.ygrid.minor_grid_line_color = null;

      fig.xaxis.major_tick_line_color = null;
      fig.xaxis.minor_tick_line_color = null;
      fig.yaxis.major_tick_line_color = null;
      fig.yaxis.minor_tick_line_color = null;
      fig.xaxis.major_label_text_font_size = '0px';
      fig.yaxis.major_label_text_font_size = '0px';
   }

   makeFeatureMap(allowedFeatures, forbiddenFeatures, predicate) {
      if (!this.tableData)
         return;
      const td = this.tableData;
      const allowed = allowedFeatures ? td.matchColsFulltext(allowedFeatures) : [...limRange(td.colCount)];
      const forbidden = forbiddenFeatures ? td.matchColsFulltext(forbiddenFeatures) : [];

      const map = new Map();
      for (let i = 0; i < td.colCount; i++) {
         const meta = td.colMetadataAt(i);
         if ((meta?.hidden ?? false) === false && allowed.includes(i) && !forbidden.includes(i) && (predicate === undefined || predicate(meta))) {
            map.set(td.colIdAt(i), td.colTitleAt(i));
         }
      }
      return map;
   }

   makeNumericFeatureMap(allowedFeatures, forbiddenFeatures) {
      return this.makeFeatureMap(allowedFeatures, forbiddenFeatures, meta => (meta.decltype === "int" || meta.decltype === "double"));
   }

   static tryNumber(val) {
      if (isNaN(val) || val === "" || val === undefined || val === null)
         return undefined;
      return Number(val);
   }

   static tryArray(val) {
      if (Array.isArray(val))
         return val;
      return undefined;
   }

   static tryParseArray(val) {
      if (typeof val === "string" && val.length)
         return val.split(",").map(item => item.trim()).map(item => isNaN(item) ? item : Number(item));
      return undefined;
   }

   static tableauColorList() {
      const obj = {
         tab_blue: '#1f77b4',
         tab_orange: '#ff7f0e',
         tab_green: '#2ca02c',
         tab_red: '#d62728',
         tab_purple: '#9467bd',
         tab_brown: '#8c564b',
         tab_pink: '#e377c2',
         tab_gray: '#7f7f7f',
         tab_olive: '#bcbd22',
         tab_cyan: '#17becf'
      };
      return obj;
   };

   static arrayMinMax(data) {
      let i = data.length;
      let min = Infinity;
      let max = -Infinity;

      while (i--) {
         max = max < data[i] ? data[i] : max;
         min = data[i] < min ? data[i] : min;
      }

      return [ min, max ];
   }

   static fixAxisRange(minMax, margins, zero) {
      let marginLo = 0, marginHi = 0;
      if (Array.isArray(margins) && margins.length === 2)
         [marginLo, marginHi] = margins;
      else if (typeof margins === "number")
         [marginLo, marginHi] = [margins, margins];
      const rng = minMax[1] - minMax[0];
      let newMin = minMax[0] - rng * marginLo;
      let newMax = minMax[1] + rng * marginHi;
      if (typeof zero === "number") {
         if (minMax[0] - rng * zero < 0 && 0 < minMax[0])
            newMin = 0;
         else if (minMax[1] < 0 && 0 < minMax[1] + rng * zero)
            newMax = 0;
      }
      else {
         if (newMin < 0 && 0 < minMax[0])
            newMin = 0;
         else if (minMax[1] < 0 && 0 < newMax)
            newMax = 0;
      }
      return [newMin, newMax];
   }

   static calculateAxisRange(data, autoscale, settingMinMax, globalRangeMinMax, margins, zero) {
      if (autoscale) {
         let [min, max] = LimGraphBokeh.arrayMinMax(data);
         if (min < max)
            return LimGraphBokeh.fixAxisRange([min, max], margins, zero);
         else {
            return LimGraphBokeh.calculateAxisRange(data, false, settingMinMax, globalRangeMinMax, margins, zero);
         }
      }
      else if (Array.isArray(settingMinMax) && settingMinMax.length === 2 && settingMinMax[0] && settingMinMax[1]) {
         return settingMinMax;
      }
      else if (typeof globalRangeMinMax?.min === "number" && typeof globalRangeMinMax?.max === "number" ) {
         return LimGraphBokeh.fixAxisRange([globalRangeMinMax.min, globalRangeMinMax.max], margins, zero);
      }
      else {
         return LimGraphBokeh.fixAxisRange(LimGraphBokeh.arrayMinMax(data), margins, zero);
      }
   }
}

/*___________________________________________________________________________*/
class LimGraphHistogram extends LimGraphBokeh {
   #graphFigure
   #figColIdX
   #figIsAutoScale
   #graphDataSources
   #dataIsCardinal
   #xmin
   #xmax
   #legendLabel
   #xAxisDefaultFeature
   #xAxisOptionValuesChanged
   #xAxisFeatureMap
   #xAxisColId
   #xAxisAllowedFeatures
   #xAxisForbiddenFeatures
   #xAxisBinCount
   #xAxisMinimum
   #xAxisMaximum
   #xAxisValues
   #xAxisOptionValueChanged
   #barWidth

   #fitNormalOptionEnabled
   #fitNormalOptionEnabledChanged
   #fitNormalOptionValue
   #fitNormalOptionValueChanged

   #autoScaleOptionEnabled
   #autoScaleOptionEnabledChanged
   #autoScaleOptionValue
   #autoScaleOptionValueChanged

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

   constructor(...args) {
      super(...args)
      const [container, pars, ...remainingArgs] = args;
      this.#graphFigure = null;
      this.#figColIdX = null;
      this.#dataIsCardinal = null;
      this.#graphDataSources = null;
      this.#legendLabel = null;
      this.#xAxisFeatureMap = new Map();
      this.#xAxisOptionValuesChanged = new LimSignal(pars?.xAxisOptionValuesChanged ? [pars.xAxisOptionValuesChanged] : []);
      this.#xAxisOptionValueChanged = new LimSignal(pars?.xAxisOptionValueChanged ? [pars.xAxisOptionValueChanged] : []);
      this.#xAxisColId = pars?.xAxisColId ?? "";
      this.#xAxisAllowedFeatures = pars?.xAxisAllowedFeatures;
      this.#xAxisForbiddenFeatures = pars?.xAxisForbiddenFeatures;
      this.#xAxisBinCount = Math.round(LimGraphBokeh.tryNumber(pars?.xAxisBinCount) ?? 20);
      this.#xAxisMinimum = LimGraphBokeh.tryNumber(pars?.xAxisMinimum);
      this.#xAxisMaximum = LimGraphBokeh.tryNumber(pars?.xAxisMaximum);
      this.#xAxisValues = LimGraphBokeh.tryArray(pars?.xAxisValues) ?? LimGraphBokeh.tryParseArray(pars?.xAxisValues);
      this.#xAxisDefaultFeature = pars?.xAxisDefaultFeature;

      this.#barWidth = LimGraphBokeh.tryNumber(pars?.barWidth) ?? 0.9;

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

      this.defaultGraphTools = ["pan", "reset", "tap", "wheel_zoom", "xpan", "xwheel_zoom"];

      if (pars?.xAxisOptionVisible ?? true) {
         this.optionList.set("xAxis", {
            type: "selection",
            title: "X axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/histo_common.svg",
         });
      }

      if (pars?.fitNormalOptionVisible ?? true) {
         this.optionList.set("fitNormal", {
            type: "option",
            title: "Fit normal distribution",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/normalDistrib_common.svg"
         });
      }

      if (pars?.autoScaleOptionVisible ?? true) {
         this.optionList.set("autoScale", {
            type: "option",
            title: "Scale to fit the data",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/luts_autoscale.svg"
         });
      }

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

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

   async update() {
      if (!this.tableData)
         return;

      this.updateFeatureList();

      const colIndex = this.tableData.colIndexById(this.#xAxisColId);
      let dataIsCardinal = this.tableData.colIsNumericAt(colIndex);
      if (!this.container?.children?.length || !this.#graphFigure || this.#dataIsCardinal !== dataIsCardinal || this.#figColIdX !== this.#xAxisColId) {
         this.#graphFigure = null;
         this.#graphDataSources = {
            histo: new Bokeh.ColumnDataSource(),
            fit: new Bokeh.ColumnDataSource()
         };
         this.fetchAndFillGraphDataSource();
      }
      else {
         this.fetchAndFillGraphDataSource();
      }
   }

   updateFeatureList() {
      if (!this.tableData)
         return;

      const td = this.tableData;
      const xAxisFeatureMap = this.makeFeatureMap(this.#xAxisAllowedFeatures, this.#xAxisForbiddenFeatures, (meta) => !meta?.jsonObject);
      if (JSON.stringify([...this.#xAxisFeatureMap.entries()]) !== JSON.stringify([...xAxisFeatureMap.entries()])) {
         this.#xAxisFeatureMap = xAxisFeatureMap;
         this.#xAxisOptionValuesChanged.emit(this);
      }

      if (![...this.#xAxisFeatureMap.keys()].includes(this.#xAxisColId) && this.#xAxisDefaultFeature) {
         const id = td.colIdAt(td.matchColsFulltext(this.#xAxisDefaultFeature)[0] ?? -1)
         if (this.#xAxisColId !== id) {
            this.#xAxisColId = id;
            this.#xAxisOptionValueChanged.emit(this);
         }
      }

      if (![...this.#xAxisFeatureMap.keys()].includes(this.#xAxisColId)) {
         const id = [...this.#xAxisFeatureMap.keys()].find(item => item[0] != '_');
         if (this.#xAxisColId !== id) {
            this.#xAxisColId = id;
            this.#xAxisOptionValueChanged.emit(this);
         }
      }
   }

   get dataColumns() {
      let ret = this.tableData.systemColIdList;
      if (this.#xAxisColId)
         ret.push(this.#xAxisColId);
      return ret;
   }

   get rowFilter() {
      let ret = [];
      if (this.#xAxisColId)
         ret.push({ op: 'valid', col: this.#xAxisColId });
      return ret;
   }

   onRowSelectionChanged(val) {
      if (this.tableRowSelection)
         this.fillGraphDataSource();
   }

   fillGraphDataSource() {
      if (!this.tableData)
         return;

      let data = {};
      let fit = { x: [], y: [] };
      const rowSelection = 1 < this.rowSelection.length ? this.rowSelection : [];
      const { color_graph_default, color_graph_selection, color_graph_nonselection } = this.selection_styles();
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();
      let fitNormalEnabled = false;
      const colIndex = this.tableData.colIndexById(this.#xAxisColId);
      let dataIsCardinal = this.tableData.colIsNumericAt(colIndex);
      if (0 <= colIndex && dataIsCardinal) {
         const xmeta = this.tableData.colMetadataAt(colIndex);
         let xmin = this.#xAxisMinimum ?? xmeta?.globalRange?.min;
         let xmax = this.#xAxisMaximum ?? xmeta?.globalRange?.max;
         data = this.tableData.columnHistogramEquidistantBinsAt(colIndex, this.#xAxisBinCount, this.#autoScaleOptionValue ? undefined : xmin, this.#autoScaleOptionValue ? undefined : xmax, rowSelection)

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

               const fitMetaFn = (mean, stdev, scale) => {
                  const term1 = scale / (stdev * Math.sqrt(2 * Math.PI));
                  return x => term1 * Math.exp(-0.5 * ((x - mean) / stdev) ** 2);
               };

               let scale = 0;
               for (let i = 0; i < data.cnt.length; i++) {
                  scale += data.cnt[i] * (data.his[i] - data.los[i]);
               }

               const [mean, stdev] = this.tableData.colDataStatsAt(colIndex, ["mean", "stdev"]);
               this.#legendLabel = `\u03bc=${mean.toFixed(3)}, \u03c3=${stdev.toFixed(4)}`
               const fn = fitMetaFn(mean, stdev, scale);
               const xrng = this.#xmax - this.#xmin;
               const step = (1.4 * xrng) / 1000;
               for (let x_ = this.#xmin - 0.2 * xrng; x_ <= this.#xmax + 0.2 * xrng; x_ += step) {
                  fit.x.push(x_)
                  fit.y.push(fn(x_));
               }
            }
            else {
               this.#legendLabel = "";
            }

            const d = (1 - this.#barWidth) * (this.#xmax - this.#xmin) / N / 2;
            data.los = data.los.map(x => x + d);
            data.his = data.his.map(x => x - d);

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

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

      this.fitNormalOptionEnabled = fitNormalEnabled;
      this.autoScaleOptionEnabled = dataIsCardinal;
   }

   createGraphFigure() {
      if (this.#graphFigure || !this.tableData || !this.container) {
         if (this.#graphFigure) {
            if (this.#dataIsCardinal) {
               if (this.#autoScaleOptionValue || this.#figIsAutoScale) {
                  if (this.#graphFigure.x_range.reset_start !== this.#xmin) {
                     this.#graphFigure.x_range.start = this.#graphFigure.x_range.reset_start = this.#xmin;
                  }
                  if (this.#graphFigure.x_range.reset_end !== this.#xmax) {
                     this.#graphFigure.x_range.end = this.#graphFigure.x_range.reset_end = this.#xmax
                  }
               }
               else {
                  this.#graphFigure.x_range.reset_start = this.#xmin;
                  this.#graphFigure.x_range.reset_end = this.#xmax;
               }
            }

            this.#graphFigure.xaxis.axis_label = this.tableData.colTitleAndUnit(this.#xAxisColId) ?? null;
            if (this.#graphFigure.legend.items.length) {
               this.#graphFigure.legend.items[0].label = this.#legendLabel;
               this.#graphFigure.legend.items[0].visible = !!this.#legendLabel;
            }
         }

         this.#figIsAutoScale = this.#autoScaleOptionValue;
         return;
      }

      let fig = null;
      let renderer = null;
      const { color_graph_default, color_graph_selection, color_graph_nonselection } = this.selection_styles();
      const { color_base, color_text, color_midlight, color_graph_data, color_graph_highlight } = this.styles();
      const colIndex = this.tableData.colIndexById(this.#xAxisColId);
      let dataIsCardinal = this.tableData.colIsNumericAt(colIndex);
      if (dataIsCardinal) {
         const xrng = new Bokeh.Range1d({ reset_start: this.#xmin, reset_end: this.#xmax, start: this.#xmin, end: this.#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"
         });

         this.styleGraphFigure(fig, this.tableData.colTitleAndUnit(this.#xAxisColId), "Count");

         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
         });

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

      }
      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.colTitleAndUnit(this.#xAxisColId), "Count");

         renderer = fig.vbar({
            x: { field: "labels" }, bottom: { field: "sel" }, top: { field: "cnt" }, width: this.#barWidth,
            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: this.#barWidth,
            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.tableRowSelection && 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;
                     this.fillGraphDataSource();
                  }
               }
            },
            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 = [];
            obj.fillGraphDataSource();
         });
      }

      this.#graphFigure = fig;
      this.#figColIdX = this.#xAxisColId
      this.#figIsAutoScale = this.#autoScaleOptionValue;
      this.#dataIsCardinal = dataIsCardinal;

      this.container.innerText = "";
      Bokeh.Plotting.show(this.#graphFigure, this.container);
   }

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

   set xAxisDefaultFeature(val) {
      if (this.#xAxisDefaultFeature === val)
         return;
      this.#xAxisDefaultFeature = val;
      this.update();
   }

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

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

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

   set xAxisBinCount(val) {
      this.#xAxisBinCount = val;
      this.update();
   }

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

   set xAxisMinimum(val) {
      this.#xAxisMinimum = val;
      this.update();
   }

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

   set xAxisMaximum(val) {
      this.#xAxisMaximum = val;
      this.update();
   }

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

   set xAxisValues(val) {
      this.#xAxisValues = val;
      this.update();
   }

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

   set xAxisAllowedFeatures(val) {
      this.#xAxisAllowedFeatures = val;
      this.update();
   }

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

   set xAxisForbiddenFeatures(val) {
      this.#xAxisForbiddenFeatures = val;
      this.update();
   }

   get state() {
      return {
         xAxisColId: this.xAxisColId,
         xAxisDefaultFeature: this.xAxisDefaultFeature,
         xAxisAllowedFeatures: this.xAxisAllowedFeatures,
         xAxisForbiddenFeatures: this.xAxisForbiddenFeatures,
         xAxisBinCount: this.xAxisBinCount,
         xAxisMinimum: this.xAxisMinimum,
         xAxisMaximum: this.xAxisMaximum,
         xAxisValues: this.xAxisValues,
      }
   }

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

   get xAxisOptionValues() {
      return [...this.#xAxisFeatureMap.entries()];
   }

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

   get xAxisOptionValue() {
      return this.xAxisColId;
   }

   set xAxisOptionValue(val) {
      if (this.#xAxisFeatureMap) {
         this.xAxisColId = val;
      }
   }

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

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

   set fitNormalOptionEnabled(val) {
      if (this.#fitNormalOptionEnabled === val)
         return;
      this.#fitNormalOptionEnabled = val
      this.#fitNormalOptionEnabledChanged.emit(this);
   }

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

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

   set fitNormalOptionValue(val) {
      if (this.#fitNormalOptionValue === val)
         return;
      this.#fitNormalOptionValue = val;
      this.#fitNormalOptionValueChanged.emit(this);
      this.#graphFigure = null;
      this.update();
   }

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

   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;
   }

   get autoScaleOptionEnabled() {
      return this.#fitNormalOptionEnabled;
   }

   set autoScaleOptionEnabled(val) {
      if (this.#autoScaleOptionEnabled === val)
         return;
      this.#autoScaleOptionEnabled = val
      this.#autoScaleOptionEnabledChanged.emit(this);
   }

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

/*___________________________________________________________________________*/
class LimGraphScatterplot extends LimGraphBokeh {
   #graphFigure
   #graphDataSource
   #graphDataColIds

   #xAxisOptionValuesChanged
   #xAxisColId
   #xAxisDefaultFeature
   #xAxisAllowedFeatures
   #xAxisForbiddenFeatures
   #xAxisFeatureMap
   #xAxisMinimum
   #xAxisMaximum
   #xAxisOptionValueChanged

   #yAxisOptionValuesChanged
   #yAxisColId
   #yAxisDefaultFeature
   #yAxisAllowedFeatures
   #yAxisForbiddenFeatures
   #yAxisFeatureMap
   #yAxisMinimum
   #yAxisMaximum
   #yAxisOptionValueChanged

   #zAxisOptionValuesChanged
   #zAxisColId
   #zAxisDefaultFeature
   #zAxisAllowedFeatures
   #zAxisForbiddenFeatures
   #zAxisFeatureMap
   #zAxisMinimum
   #zAxisMaximum
   #zAxisOptionValueChanged

   #autoScaleOptionValue
   #autoScaleOptionValueChanged

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

   constructor(...args) {
      super(...args)
      const [container, pars, ...remainingArgs] = args;
      this.#graphFigure = null;
      this.#graphDataSource = null;
      this.#graphDataColIds = [];

      this.#xAxisColId = pars?.xAxisColId ? pars.xAxisColId : "";
      this.#xAxisOptionValuesChanged = new LimSignal(pars?.xAxisOptionValuesChanged ? [pars.xAxisOptionValuesChanged] : []);
      this.#xAxisOptionValueChanged = new LimSignal(pars?.xAxisOptionValueChanged ? [pars.xAxisOptionValueChanged] : []);
      this.#xAxisDefaultFeature = pars?.xAxisDefaultFeature ? pars.xAxisDefaultFeature : undefined;
      this.#xAxisAllowedFeatures = pars?.xAxisAllowedFeatures ? pars.xAxisAllowedFeatures : undefined;
      this.#xAxisForbiddenFeatures = pars?.xAxisForbiddenFeatures ? pars.xAxisForbiddenFeatures : undefined;
      this.#xAxisFeatureMap = new Map();
      this.#xAxisMinimum = LimGraphBokeh.tryNumber(pars?.xAxisMinimum);
      this.#xAxisMaximum = LimGraphBokeh.tryNumber(pars?.xAxisMaximum);

      this.#yAxisColId = pars?.yAxisColId ? pars.yAxisColId : "";
      this.#yAxisOptionValuesChanged = new LimSignal(pars?.yAxisOptionValuesChanged ? [pars.yAxisOptionValuesChanged] : []);
      this.#yAxisOptionValueChanged = new LimSignal(pars?.yAxisOptionValueChanged ? [pars.yAxisOptionValueChanged] : []);
      this.#yAxisDefaultFeature = pars?.yAxisDefaultFeature ? pars.yAxisDefaultFeature : undefined;
      this.#yAxisAllowedFeatures = pars?.yAxisAllowedFeatures ? pars.yAxisAllowedFeatures : undefined;
      this.#yAxisForbiddenFeatures = pars?.yAxisForbiddenFeatures ? pars.yAxisForbiddenFeatures : undefined;
      this.#yAxisFeatureMap = new Map();
      this.#yAxisMinimum = LimGraphBokeh.tryNumber(pars?.yAxisMinimum);
      this.#yAxisMaximum = LimGraphBokeh.tryNumber(pars?.yAxisMaximum);

      this.#zAxisOptionValuesChanged = new LimSignal(pars?.zAxisOptionValuesChanged ? [pars.zAxisOptionValuesChanged] : []);
      this.#zAxisOptionValueChanged = new LimSignal(pars?.zAxisOptionValueChanged ? [pars.zAxisOptionValueChanged] : []);
      this.#zAxisDefaultFeature = pars?.zAxisDefaultFeature ? pars.zAxisDefaultFeature : undefined;
      this.#zAxisColId = pars?.zAxisColId ? pars.zAxisColId : (this.#zAxisDefaultFeature ?? "");
      this.#zAxisAllowedFeatures = pars?.zAxisAllowedFeatures ? pars.zAxisAllowedFeatures : undefined;
      this.#zAxisForbiddenFeatures = pars?.zAxisForbiddenFeatures ? pars.zAxisForbiddenFeatures : undefined;
      this.#zAxisFeatureMap = new Map();
      this.#zAxisMinimum = LimGraphBokeh.tryNumber(pars?.zAxisMinimum);
      this.#zAxisMaximum = LimGraphBokeh.tryNumber(pars?.zAxisMaximum);
      this.defaultGraphTools = ["box_select", "lasso_select", "hover", "pan", "reset", "wheel_zoom"];
      if (!this.iconres)
         this.iconres = LimGraphScatterplot.iconres;
      if (!this.name)
         this.name = LimGraphScatterplot.name;

      if (pars?.xAxisOptionVisible ?? true) {
         this.optionList.set("xAxis", {
            type: "selection",
            title: "X axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_x.svg"
         });
      }

      if (pars?.yAxisOptionVisible ?? true) {
         this.optionList.set("yAxis", {
            type: "selection",
            title: "Y axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_y.svg"
         });
      }

      if (pars?.zAxisOptionVisible ?? true) {
         this.optionList.set("zAxis", {
            type: "selection",
            title: "Z axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_z.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"
         });
      }

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

   update() {
      if (!this.tableData)
         return;

      this.updateFeatureList();
      if (JSON.stringify(this.#graphDataColIds) !== JSON.stringify([this.#xAxisColId, this.#yAxisColId, this.#zAxisColId]) || !this.#graphFigure || !this?.container?.children?.length) {
         this.#graphFigure = null;
         this.#graphDataSource = new Bokeh.ColumnDataSource();
         this.#graphDataSource.selected.js_property_callbacks = {
            "change:indices": [new Bokeh.CustomJS({
               args: {
                  fn: (sel) => {
                     if (this.tableRowSelection && Array.isArray(sel)) {
                        try { this.rowSelection = sel; }
                        catch { }
                     }
                  }
               },
               code: "fn(this.indices)"
            })]
         };
         this.#graphDataColIds = [this.#xAxisColId, this.#yAxisColId, this.#zAxisColId];
         this.fetchAndFillGraphDataSource();
      }
      else {
         this.fetchAndFillGraphDataSource();
         if (this.#graphFigure && this?.autoScaleOptionValue) {
            const xmeta = this.tableData ? this.tableData.colMetadata(this.#xAxisColId) : undefined;
            [this.#graphFigure.x_range.start, this.#graphFigure.x_range.end] = LimGraphBokeh.calculateAxisRange(this.#graphDataSource.data.x, this.#autoScaleOptionValue, [this.#xAxisMinimum, this.#xAxisMaximum], xmeta?.globalRange, 0.05, 0.2);
            const ymeta = this.tableData ? this.tableData.colMetadata(this.#yAxisColId) : undefined;
            [this.#graphFigure.y_range.start, this.#graphFigure.y_range.end] = LimGraphBokeh.calculateAxisRange(this.#graphDataSource.data.y, this.#autoScaleOptionValue, [this.#yAxisMinimum, this.#yAxisMaximum], ymeta?.globalRange, 0.05, 0.2);
         }
      }
   }

   updateFeatureList() {
      if (!this.tableData)
         return;

      const td = this.tableData;
      const axisColChanged = [false, false, false];
      this.#xAxisFeatureMap = this.makeNumericFeatureMap(this.#xAxisAllowedFeatures, this.#xAxisForbiddenFeatures);
      this.#xAxisOptionValuesChanged.emit(this);

      if (![...this.#xAxisFeatureMap.keys()].includes(this.#xAxisColId) && this.#xAxisDefaultFeature) {
         this.#xAxisColId = td.colIdAt(td.matchColsFulltext(this.#xAxisDefaultFeature)[0] ?? -1);
         axisColChanged[0] = true;
      }

      if (![...this.#xAxisFeatureMap.keys()].includes(this.#xAxisColId)) {
         const vals = [...this.#xAxisFeatureMap.keys()];
         this.#xAxisColId = vals.find(item => item[0] != '_') ?? "";
         axisColChanged[0] = true;
      }

      if (axisColChanged[0])
         this.#xAxisOptionValueChanged.emit(this);

      this.#yAxisFeatureMap = this.makeNumericFeatureMap(this.#yAxisAllowedFeatures, this.#yAxisForbiddenFeatures);
      this.#yAxisOptionValuesChanged.emit(this);

      if (![...this.#yAxisFeatureMap.keys()].includes(this.#yAxisColId) && this.#yAxisDefaultFeature) {
         this.#yAxisColId = td.colIdAt(td.matchColsFulltext(this.#yAxisDefaultFeature)[0] ?? -1);
         axisColChanged[1] = true;
      }

      if (![...this.#yAxisFeatureMap.keys()].includes(this.#yAxisColId)) {
         const vals = [...this.#yAxisFeatureMap.keys()];
         this.#yAxisColId = vals.find(item => item[0] != '_' && item != this.#xAxisColId, this) ?? "";
         axisColChanged[1] = true;
      }

      if (axisColChanged[1])
         this.#yAxisOptionValueChanged.emit(this);

      this.#zAxisFeatureMap = this.makeFeatureMap(this.#zAxisAllowedFeatures, this.#zAxisForbiddenFeatures, (meta) => !meta?.jsonObject);
      this.#zAxisOptionValuesChanged.emit(this);

      if (this.#zAxisColId && ![...this.#zAxisFeatureMap.keys()].includes(this.#zAxisColId) && this.#zAxisDefaultFeature) {
         this.#zAxisColId = td.colIdAt(td.matchColsFulltext(this.#zAxisDefaultFeature)[0] ?? -1);
         axisColChanged[2] = true;
      }

      if (axisColChanged[2])
         this.#zAxisOptionValueChanged.emit(this);
   }

   get dataColumns() {
      let ret = this.tableData.systemColIdList;
      if (this.#xAxisColId)
         ret.push(this.#xAxisColId);
      if (this.#yAxisColId)
         ret.push(this.#yAxisColId);
      if (this.#zAxisColId)
         ret.push(this.#zAxisColId);
      return ret;
   }

   get rowFilter() {
      let ret = [];
      if (this.#xAxisColId)
         ret.push({ op: 'valid', col: this.#xAxisColId });
      if (this.#yAxisColId)
         ret.push({ op: 'valid', col: this.#yAxisColId });
      if (this.#zAxisColId)
         ret.push({ op: 'valid', col: this.#zAxisColId });
      return ret;
   }

   onRowSelectionChanged(val) {
      if (this.tableRowSelection)
         this.#graphDataSource.selected.indices = val;
   }

   fillGraphDataSource() {
      if (!this.tableData)
         return;

      const xColIndex = this.tableData.colIndexById(this.#xAxisColId);
      const yColIndex = this.tableData.colIndexById(this.#yAxisColId);
      const zColIndex = this.tableData.colIndexById(this.#zAxisColId);
      if (xColIndex < 0 || yColIndex < 0)
         return;

      this.tableData.rowTitlesLong

      const data = { x: this.tableData.colDataAt(xColIndex), y: this.tableData.colDataAt(yColIndex), ident: this.tableData.rowTitlesLong };
      const zmeta = this.tableData ? this.tableData.colMetadata(this.#zAxisColId) : undefined;
      if (0 <= zColIndex && zmeta) {

         data.z = this.tableData.colDataAt(zColIndex);

         let colors = [];
         const low_color = "purple";
         const high_color = "orange";
         const interpolator = d3.interpolateRgb(low_color, high_color);
         if (zmeta.decltype === "double" || zmeta.decltype === "int") {
            const [zmin, zmax] = LimGraphBokeh.calculateAxisRange(data.z, this.#autoScaleOptionValue, [this.#zAxisMinimum, this.#zAxisMaximum], zmeta?.globalRange);
            const rng = zmax - zmin;

            for (let i = 0; i < data.z.length; i++) {
               colors.push(interpolator((data.z[i] - zmin) / rng));
            }

            const npal = 20;
            const palette = [];
            for (let i = 0; i < npal; i++) {
               palette.push(interpolator((i / (npal - 1))));
            }
            this.color_mapper = new Bokeh.LinearColorMapper({ low: zmin, high: zmax, palette });
            if(this.color_bar)
               this.color_bar.color_mapper = new Bokeh.LinearColorMapper({ low: zmin, high: zmax, palette });
         }
         else {
            const labels = [... new Set(data.z)];
            for (const val of data.z) {
               colors.push(interpolator(Math.max(0, labels.indexOf(val)) / (labels.length - 1)));
            }

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

         data.color = colors;
      }

      this.#graphDataSource.data = data;
      if (this.tableRowSelection)
         this.#graphDataSource.selected.indices = this.rowSelection;
   }

   createGraphFigure() {
      if (this.#graphFigure || !this.tableData || !this.container)
         return;

      const xColIndex = this.tableData.colIndexById(this.#xAxisColId);
      const yColIndex = this.tableData.colIndexById(this.#yAxisColId);
      if (xColIndex < 0 || yColIndex < 0)
         return;

      const { color_base, color_text, color_midlight  } = this.styles();
      const { color_graph_default, color_graph_selection, color_graph_nonselection } = this.selection_styles();

      const xmeta = this.tableData ? this.tableData.colMetadata(this.#xAxisColId) : undefined;
      const [xmin, xmax] = LimGraphBokeh.calculateAxisRange(this.#graphDataSource.data.x, this.#autoScaleOptionValue, [this.#xAxisMinimum, this.#xAxisMaximum], xmeta?.globalRange, 0.05, 0.2);

      const ymeta = this.tableData ? this.tableData.colMetadata(this.#yAxisColId) : undefined;
      const [ymin, ymax] = LimGraphBokeh.calculateAxisRange(this.#graphDataSource.data.y, this.#autoScaleOptionValue, [this.#yAxisMinimum, this.#yAxisMaximum], ymeta?.globalRange, 0.05, 0.2);

      const zmeta = this.tableData ? this.tableData.colMetadata(this.#zAxisColId) : undefined;

      const xrng = new Bokeh.Range1d({ start: xmin, end: xmax });
      const yrng = new Bokeh.Range1d({ start: ymin, end: ymax });
      const fig = Bokeh.Plotting.figure({ title: this.title,
         title: this.title,
         x_range: xrng, y_range: yrng,
         tools: this.graphTools.filter(item => item !== "hover").join(","),
         sizing_mode: "stretch_both"
      });
      let color_bar;

      this.styleGraphFigure(fig,
         xmeta.units ? `${xmeta.title} [${xmeta.units}]` : `${xmeta.title}`,
         ymeta.units ? `${ymeta.title} [${ymeta.units}]` : `${ymeta.title}`);

      if (this.#graphDataSource.data.z && zmeta) {

         const renderer = fig.scatter({
            x: { field: "x" }, y: { field: "y" }, size: 10,
            source: this.#graphDataSource,
            fill_color: { field: "color" }, fill_alpha: 0.8, line_color: null,
            selection_fill_color: { field: "color" }, selection_fill_alpha: 0.9, selection_line_color: "black", selection_line_alpha: 1,
            nonselection_fill_color: { field: "color" }, nonselection_fill_alpha: 0.4, nonselection_line_color: null,

         });

         color_bar = new Bokeh.ColorBar({
            color_mapper: this.color_mapper,
            label_standoff: 10,
            major_tick_line_alpha: 0,
            background_fill_color: color_base,
            major_label_text_color: color_text,
            border_line_color: color_midlight });
         fig.add_layout(color_bar, 'right');

         if (this.graphTools.includes("hover")) {
            fig.add_tools(new Bokeh.HoverTool({
               renderers: [renderer],
               tooltips: (`
               <table style="color:black; font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
               <tr>
                  <td rowspan="5" style="background: @color; width: 7px"></td>
                  <td rowspan="5" style="width: 2px">
                  <td colspan="4" style="font-size: 11px; border-bottom: 1px solid @color;"><b>@ident</b></td>
               </tr>
               <tr><td><b>X:</b></td><td>${xmeta.title}</td><td class="text-right">@x{0.[000]}</td><td>${xmeta.units ? xmeta.units : ""}</td></tr>
               <tr><td><b>Y:</b></td><td>${ymeta.title}</td><td class="text-right">@y{0.[000]}</td><td${ymeta.units ? ymeta.units : ""}</td></tr>
               <tr><td><b>Color:</b></td><td>${zmeta.title}</td><td class="text-right">@z{0.[000]}</td><td${zmeta.units ? zmeta.units : ""}</td></tr>`)
            }));
         }
      }

      else {
         const renderer = fig.scatter({
            x: { field: "x" }, y: { field: "y" }, size: 10,
            source: this.#graphDataSource,
            fill_color: color_graph_default, fill_alpha: 0.7, line_color: null,
            selection_fill_color: color_graph_selection, selection_fill_alpha: 0.9, selection_line_color: "black", selection_line_alpha: 1,
            nonselection_fill_color: color_graph_nonselection, nonselection_fill_alpha: 0.4, nonselection_line_color: null,

         });

         if (this.graphTools.includes("hover")) {
            fig.add_tools(new Bokeh.HoverTool({
               renderers: [renderer],
               tooltips: (`
                  <table style="color:black; font-size: 10px; margin: 0; margin-top: 0.5em; border-collapse: collapse;">
                  <tr>
                     <td rowspan="5" style="background: ${integerToRGBA(color_graph_default)}; width: 7px"></td>
                     <td rowspan="5" style="width: 2px">
                     <td colspan="4" style="font-size: 11px; border-bottom: 1px solid ${integerToRGBA(color_graph_default)};"><b>@ident</b></td>
                  </tr>
                  <tr><td><b>X:</b></td><td>${xmeta.title}</td><td class="text-right">@x</td><td>${xmeta.units ? xmeta.units : ""}</td></tr>
                  <tr><td><b>Y:</b></td><td>${ymeta.title}</td><td class="text-right">@y</td><td${ymeta.units ? ymeta.units : ""}</td></tr>`)
            }));
         }
      }

      this.container.innerText = "";
      Bokeh.Plotting.show(this.#graphFigure = fig, this.container);
      this.color_bar = color_bar;
   }

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

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

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

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

   set xAxisAllowedFeatures(val){
      this.#xAxisAllowedFeatures = val;
      this.update();
   }

   get xAxisForbiddenFeatures(){
      return this.#xAxisForbiddenFeatures;
   }
   set xAxisForbiddenFeatures(val){
      this.#xAxisForbiddenFeatures = val;
      this.update();
   }

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

   set xAxisMaximum(val){
      this.#xAxisMaximum = val;
   }

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

   set xAxisMinimum(val){
      this.#xAxisMinimum = val;
   }

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

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

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

   get yAxisAllowedFeatures(){
      return this.#yAxisAllowedFeatures;
   }
   set yAxisAllowedFeatures(val){
      this.#yAxisAllowedFeatures = val;
      this.update();
   }

   get yAxisForbiddenFeatures(){
      return this.#yAxisForbiddenFeatures;
   }
   set yAxisForbiddenFeatures(val) {
      this.#yAxisForbiddenFeatures = val;
      this.update();
   }

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

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

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

   set zAxisColId(val) {
      if (this.#zAxisColId === val)
         return;
      this.#zAxisColId = val;
      this.#zAxisOptionValueChanged.emit(this);
      this.update();
   }

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

   get zAxisAllowedFeatures(){
      return this.#zAxisAllowedFeatures;
   }
   set zAxisAllowedFeatures(val){
      this.#zAxisAllowedFeatures = val;
      this.upate();
   }

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

   set zAxisForbiddenFeatures(val){
      this.#zAxisForbiddenFeatures = val;
      this.update();
   }

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

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

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

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

   get xAxisOptionValues() {
      return [...this.#xAxisFeatureMap.entries()];
   }

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

   get xAxisOptionValue() {
      return this.xAxisColId;
   }

   set xAxisOptionValue(val) {
      this.xAxisColId = val;
   }

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

   get yAxisOptionValues() {
      return [...this.#yAxisFeatureMap.entries()];
   }

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

   get yAxisOptionValue() {
      return this.yAxisColId;
   }

   set yAxisOptionValue(val) {
      this.yAxisColId = val;
   }

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

   get zAxisOptionValues() {
      return [ ["-", "&lt;none&gt;"], ...this.#zAxisFeatureMap.entries()];
   }

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

   get zAxisOptionValue() {
      return this.zAxisColId ? this.zAxisColId : "-";
   }

   set zAxisOptionValue(val) {
      this.zAxisColId = val === "-" ? "" : val;
   }

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

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

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

      if (!this.#autoScaleOptionValue) {
         const xmeta = this.tableData ? this.tableData.colMetadata(this.#xAxisColId) : undefined;
         if (xmeta)
            [this.#graphFigure.x_range.start, this.#graphFigure.x_range.end] = LimGraphBokeh.calculateAxisRange(this.#graphDataSource.data.x, this.#autoScaleOptionValue, [this.#xAxisMinimum, this.#xAxisMaximum], xmeta?.globalRange, 0.05, 0.2);
         const ymeta = this.tableData ? this.tableData.colMetadata(this.#yAxisColId) : undefined;
         if (ymeta)
            [this.#graphFigure.y_range.start, this.#graphFigure.y_range.end] = LimGraphBokeh.calculateAxisRange(this.#graphDataSource.data.y, this.#autoScaleOptionValue, [this.#yAxisMinimum, this.#yAxisMaximum], ymeta?.globalRange, 0.05, 0.2);
      }
   }

   get autoScaleOptionValueChanged() {
      return this.#autoScaleOptionValueChanged
   }
}

/*___________________________________________________________________________*/
class LimGraphFittedData extends LimGraphBokeh {
   #graphFigure
   #fitEquationFeatureMap
   #fitEquationColId
   #xAxisIsLog
   #fitEquationAllowedFeatures
   #fitEquationForbiddenFeatures

   static name = "Dose-response";
   static iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/doseResponse_common.svg";

   constructor(...args) {
      super(...args)
      const [container, pars, ...remainingArgs] = args;
      this.graphDataSources = null;

      this.#fitEquationFeatureMap = new Map();
      this.fitEquationDefaultFeature = pars?.fitEquationDefaultFeature;
      this.#fitEquationAllowedFeatures = pars?.fitEquationAllowedFeatures;
      this.#fitEquationForbiddenFeatures = pars?.fitEquationForbiddenFeatures;
      this.defaultGraphTools = ["hover", "pan", "reset", "tap", "wheel_zoom"];
      if (!this.iconres)
         this.iconres = LimGraphFittedData.iconres;
      if (!this.name)
         this.name = LimGraphFittedData.name;

      if (pars?.fitEquationOptionVisible ?? true) {
         this.optionList.set("fitEquation", {
            type: "selection",
            title: "Fit equation",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/doseResponse_common.svg"
         });
      }
      if (true) {
         this.optionList.set("errorBarsVisible", {
            type: "option",
            title: "Error bars visible",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_show_int.svg"
         });
      }
      if (true) {
         this.optionList.set("legendVisible", {
            type: "option",
            title: "Legend visible",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_legend.svg"
         });
      }

      Object.defineProperties(this, {
         fitEquationOptionValues: {
            get() { return [...this.#fitEquationFeatureMap.entries()]; }
         },
         fitEquationOptionValuesChanged: {
            value: new LimSignal(pars?.fitEquationOptionValuesChanged ? [pars.fitEquationOptionValuesChanged] : []),
            writable: false
         },
         fitEquationOptionValue: {
            get() { return this.fitEquationColId; },
            set(val) { this.fitEquationColId = val; }
         },
         fitEquationOptionValueChanged: {
            value: new LimSignal(pars?.fitEquationOptionValueChanged ? [pars.fitEquationOptionValueChanged] : []),
            writable: false
         }
      });

      this.errorBarsVisible = true;
      Object.defineProperties(this, {
         errorBarsVisibleOptionValue: {
            get() { return this.errorBarsVisible; },
            set(val) {
               if (this.errorBarsVisible === val)
                  return;
               this.errorBarsVisible = val;
               this.update();
            }
         },
         errorBarsVisibleOptionValueChanged: {
            value: new LimSignal(pars?.errorBarsVisibleOptionValueChanged ? [pars.errorBarsVisibleOptionValueChanged] : []),
            writable: false
         },
         errorBarsVisibleOptionEnabled: {
            get() { return !!(this.yAxisErrorBarsColId ?? this.yAxisErrorBarsDefaultColId); },
         },
         errorBarsVisibleOptionEnabledChanged: {
            value: new LimSignal(pars?.errorBarsVisibleOptionEnabledChanged ? [pars.errorBarsVisibleOptionEnabledChanged] : []),
            writable: false
         },
      });

      this.legendVisibility = "inside";
      Object.defineProperties(this, {
         legendVisibleOptionValue: {
            get() { return this.legendVisible; },
            set(val) {
               if (this.legendVisible === val)
                  return;
               this.legendVisibility = val ? "inside" : "hidden";
            }
         },
         legendVisibleOptionValueChanged: {
            value: new LimSignal(pars?.legendVisibleOptionValueChanged ? [pars.legendVisibleOptionValueChanged] : []),
            writable: false
         }
      });
   }

   update() {
      if (!this.tableData)
         return;
      this.updateFeatureList();
      this.fetchAndFillGraphDataSource();
   }

   updateFeatureList() {
      if (!this.tableData)
         return;

      const td = this.tableData;
      const fitEquationFeatureMap = this.makeFeatureMap(this.#fitEquationAllowedFeatures, this.#fitEquationForbiddenFeatures, (meta) => meta?.jsonObject === "fit");

      if (JSON.stringify([...this.#fitEquationFeatureMap]) !== JSON.stringify([...fitEquationFeatureMap])) {
         this.#fitEquationFeatureMap = fitEquationFeatureMap;
         this.fitEquationOptionValuesChanged.emit(this);
      }

      let fitEquationColId = this.#fitEquationColId;
      if (![...this.#fitEquationFeatureMap.keys()].includes(fitEquationColId) && this.fitEquationDefaultFeature) {
         fitEquationColId = td.colIdAt(td.colIndexByFullText(this.fitEquationDefaultFeature));
      }

      if (![...this.#fitEquationFeatureMap.keys()].includes(fitEquationColId) && this.#fitEquationFeatureMap.size) {
         fitEquationColId = [...this.#fitEquationFeatureMap.keys()][0];
      }

      if (![...this.#fitEquationFeatureMap.keys()].includes(fitEquationColId))
         fitEquationColId = null;

      if (JSON.stringify(this.#fitEquationColId) !== JSON.stringify(fitEquationColId)) {
         this.#fitEquationColId = fitEquationColId;
         this.fitEquationOptionValueChanged.emit(this);
      }

      this.metaFit = null;
      this.xAxisDefaultColId = null;
      this.yAxisDefaultColId = null;
      this.yAxisErrorBarsDefaultColId = null;
      this.legendRawDataLabel = 'Data';

      if (this.#fitEquationColId) {
         const meta = td.colMetadata(this.#fitEquationColId);
         if (td.colIdList.includes(meta?.xColId ?? ""))
            this.xAxisDefaultColId = meta.xColId;
         if (td.colIdList.includes(meta?.yColId ?? ""))
            this.yAxisDefaultColId = meta.yColId;
         this.metaFit = meta.fit;
      }

      const yAxisColId = this.yAxisColId ?? this.yAxisDefaultColId;
      if (yAxisColId) {
         let errColIndex = [
            td.colMetadataList.findIndex(meta => meta?.aggregated?.startsWith?.("StErr") && meta?.duplicatedFrom === yAxisColId),
            td.colMetadataList.findIndex(meta => meta?.aggregated?.startsWith?.("StDev") && meta?.duplicatedFrom === yAxisColId)
         ].find(item => 0 <= item);

         if (0 <= errColIndex) {
            this.yAxisErrorBarsDefaultColId = td.colIdAt(errColIndex);
            this.legendRawDataLabel = `Data with S${td.colMetadataAt(errColIndex).aggregated[2]}`;
         }
      }

      this.errorBarsVisibleOptionEnabledChanged.emit(this);

      if (typeof this.#xAxisIsLog === "undefined")
         this.#xAxisIsLog = true;
   }

   get dataColumns() {
      let ret = this.tableData.systemColIdList.concat(this.tableData.groupedBy);
      ret.push(this.#fitEquationColId);
      ret.push(this.xAxisColId ?? this.xAxisDefaultColId);
      ret.push(this.yAxisColId ?? this.yAxisDefaultColId);
      ret.push(this.yAxisErrorBarsColId ?? this.yAxisErrorBarsDefaultColId);
      return [...new Set(ret.filter(item => this.tableData.hasColId(item))).values()];
   }

   get rowFilter() {
      let ret = [];
      if (this.#fitEquationColId)
         ret.push({ op: 'valid', col: this.#fitEquationColId });
      const xAxisColId = this.xAxisColId ?? this.xAxisDefaultColId;
      if (xAxisColId) {
         if (this.#xAxisIsLog)
            ret.push({ op: 'gt', vala: 0, colb: xAxisColId });
         else
            ret.push({ op: 'valid', col: xAxisColId });
      }
      const yAxisColId = this.yAxisColId ?? this.yAxisDefaultColId;
      if (yAxisColId)
         ret.push({ op: 'valid', col: yAxisColId });
      //const yAxisErrorBarsColId = this.yAxisErrorBarsColId ?? this.yAxisErrorBarsDefaultColId;
      //if (yAxisErrorBarsColId)
      //   ret.push({ op: 'valid', col: yAxisErrorBarsColId });
      return ret;
   }

   fillGraphDataSource() {
      if (!this.tableData)
         return;

      this.graphDataLimits = { l: 0, r: 1, b: 0, t: 1 };
      const graphDataLimits = { l: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: Number.MAX_VALUE, t: -Number.MAX_VALUE };

      const td = this.tableData;
      const xAxisColId = this.xAxisColId ?? this.xAxisDefaultColId;
      const yAxisColId = this.yAxisColId ?? this.yAxisDefaultColId;
      const yAxisErrorBarsColId = this.yAxisErrorBarsColId ?? this.yAxisErrorBarsDefaultColId;
      const fitHasData = 0 < td.rowCount;

      this.error_message = "Graph is not available";
      if (yAxisColId) {
         const meta = td.colMetadata(yAxisColId);
         let error_message = meta?.fits?.[0]?.fitErrorMessage?.[0] ?? "";
         if(error_message)
            this.error_message = "Graph is not available. " + error_message;
      }

      if (!this.#fitEquationColId || !fitHasData || !xAxisColId || !yAxisColId) {
         this.#graphFigure = null;
         this.graphDataSources = null;
         this.graphFitRenderers = [];
         this.createGraphFigure();
         return;
      }

      const xAxisIsLog = true;
      const yAxisIsLog = false;
      const groups = td.groups;
      let callCreateGraph = false;
      const errorBarsAddedOrRemoved = Array.isArray(this?.graphDataSources?.[0]?.raw?.data?.err) != !!yAxisErrorBarsColId;
      if (!this.#graphFigure || !this?.container?.children?.length || this?.graphDataSources?.length != groups.length || this?.graphDataSources?.length === 0 || errorBarsAddedOrRemoved) {
         this.#graphFigure = null;
         this.graphFitRenderers = groups.map(() => null);
         this.graphDataSources = groups.map(() => ({
            raw: new Bokeh.ColumnDataSource(),
            fit: new Bokeh.ColumnDataSource(),
            pts: new Bokeh.ColumnDataSource()
         }));
         callCreateGraph = true;
      }

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

      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 colors = Object.values(this.colorList(/*this.seriesColorList*/));
      this.seriesNames = groups.length ? groups.map((rows, index) => groupName(rows[0])) : [""];
      this.seriesErrors = groups.length ? groups.map(() => "") : [""];
      this.seriesColorsData = 1 < groups.length ? groups.map((_, index) => colors[index % groups.length]) : [color_graph_data];
      this.seriesColorsFit = 1 < groups.length ? groups.map((_, index) => colors[index % groups.length]) : [color_graph_highlight];
      this.legendRawGroupDataLabels = this.seriesNames.map(item => { return item ? `${item}: ${this.legendRawDataLabel}` : this.legendRawDataLabel });

      const xAxisMeta = td.colMetadata(xAxisColId);
      const xAxisPrintFn = LimTableData.makePrintFunction(xAxisMeta);
      const xAxisTitle = td.colTitle(xAxisColId);
      const xAxisUnits = td.colUnit(xAxisColId) ?? "";
      const yAxisMeta = td.colMetadata(yAxisColId);
      const yAxisPrintFn = LimTableData.makePrintFunction(yAxisMeta);
      const yAxisTitle = td.colTitle(yAxisColId);
      const yAxisUnits = td.colUnit(yAxisColId) ?? "";

      let [sumFitLeftVal, sumFitRightVal] = [0, 0];
      for (let g = 0; g < groups.length; g++) {
         const serieName = this.seriesNames[g];
         const serieColorData = this.seriesColorsData[g];
         const serieColorFit = this.seriesColorsFit[g];

         const fitColIndex = td.colIndexById(this.#fitEquationColId);
         const fdata = td.dataColumnList[fitColIndex][groups[g][0]];
         const xdata = td.colData(xAxisColId, groups[g]);
         const ydata = td.colData(yAxisColId, groups[g]);
         const data = {
            x: xdata,
            y: ydata,
            r: groups[g]
         };
         data.color = data.x.map(() => serieColorData);
         data.serie = data.x.map(() => this.seriesNames[g]);

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

         if (yAxisErrorBarsColId) {
            const errordata = this.tableData.colData(yAxisErrorBarsColId, groups[g]);
            data.err = errordata;
            data.errlo = data.y.map((item, index) => item - errordata[index]);
            data.errhi = data.y.map((item, index) => item + errordata[index]);
            graphDataLimits.b = Math.min(graphDataLimits.b, ...data.errlo);
            graphDataLimits.t = Math.max(graphDataLimits.t, ...data.errhi);
         }

         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="${serieColorData}" stroke="${color_text}" /></svg>
               </span>
               <span>${serieName === "" ? "Data" : serieName}</span>
            </div>
            <table style="font-size:11px;margin:9px; ">
            <tr>
               <td style="padding:2px 5px;">${xAxisTitle}</td>
               <td style="padding:2px 1px 2px 5px;text-align:right;">${xAxisPrintFn(data.x[i])}</td>
               <td style="padding:2px 5px 2px 1px;">${xAxisUnits}</td>
            </tr>
            <tr>
               <td style="padding:2px 5px;">${yAxisTitle}</td>
               <td style="padding:2px 1px 2px 5px;text-align:right;">${yAxisPrintFn(data.y[i])}</td>
               <td style="padding:2px 5px 2px 1px;">${yAxisUnits}</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;">${yAxisUnits}</td>
            </tr>
            </table>
            </div>
            </div>`);
         }

         data.tooltip = tooltips;

         this.graphDataSources[g].raw.data = data;

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

         if (this.metaFit && fitObject?.data) {
            let fitMetaFn;
            eval(`fitMetaFn = ${this.metaFit.fnJsExpr}`);

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

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

            if (has_valid_params) {
               fn = fitMetaFn(...params);
               params[0] = params[0] ?? Infinity;
               if (xAxisIsLog) {
                  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));
                  }
                  graphDataLimits.l = Math.min(graphDataLimits.l, xlo / 10);
                  graphDataLimits.r = Math.max(graphDataLimits.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));
                  }
                  graphDataLimits.l = Math.min(graphDataLimits.l, xlo - tRange);
                  graphDataLimits.r = Math.max(graphDataLimits.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 = this.metaFit.paramNames.map(item => `<td style="padding:2px 5px;text-align: right;">${item}</td>`).join("");
            const row0 = fitObject.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 < this.metaFit.paramErrorNames.length; i++) {
               const row = fitObject.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;">${this.metaFit.paramErrorNames[i]}</td>${row}</tr>`);
            }

            const goodness = [];
            for (let i = 0; i < this.metaFit.fitGoodnessNames.length; i++) {
               goodness.push(`<tr><td style="padding:2px 5px;">${this.metaFit.fitGoodnessNames[i]}</td><td style="padding:2px 5px;text-align:right;">${fmt(fitObject.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:${serieColorFit};stroke-width:3" /></svg></span><span>${serieName === "" ? "Fit" : serieName}</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>`;

            this.graphDataSources[g].fit.data = {
               x: fitx,
               y: fity,
               color: fitx.map(() => serieColorFit),
               serie: fitx.map(() => serieName),
               tooltip: fitx.map(() => tooltip)
            };
            graphDataLimits.b = Math.min(graphDataLimits.b, ...fity);
            graphDataLimits.t = Math.max(graphDataLimits.t, ...fity);

            sumFitLeftVal += fity?.[0] ?? 0;
            sumFitRightVal += fity?.[fity?.length - 1] ?? 0;

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

      this.legendDefaultLocation = sumFitLeftVal < sumFitRightVal ? "top_left" : "top_right";

      const ymargin = 0.05 * (graphDataLimits.t - graphDataLimits.b);
      graphDataLimits.b -= ymargin;
      graphDataLimits.t += ymargin;
      this.graphDataLimits = graphDataLimits;

      if (callCreateGraph)
         this.createGraphFigure();

      else if (this.#graphFigure) {
         this.#graphFigure.legend.visible = this.legendVisible;
         this.#graphFigure.legend.location = this.legendDefaultLocation;

         this?.dataRenderers?.forEach((renderer, index) => {
            const legendItem = this.#graphFigure.legend.items.find(item => item.renderers.includes(renderer));
            if (legendItem) {
               legendItem.label = this.legendRawGroupDataLabels[index]
            }
         });

         this?.whiskerLayouts?.forEach(item => item.visible = this.errorBarsVisible);

         const newTitleX = td.colTitleAndUnit(xAxisColId);
         const newTitleY = td.colTitleAndUnit(yAxisColId);
         if (this.#graphFigure.xaxis.axis_label !== newTitleX || this.#graphFigure.yaxis.axis_label !== newTitleY) {
            this.#graphFigure.xaxis.axis_label = newTitleX;
            this.#graphFigure.yaxis.axis_label = newTitleY;
            this.#graphFigure.x_range.start = this.graphDataLimits.l;
            this.#graphFigure.x_range.end = this.graphDataLimits.r;
            this.#graphFigure.y_range.start = this.graphDataLimits.b;
            this.#graphFigure.y_range.end = this.graphDataLimits.t;
         }
         else {
            this.#graphFigure.x_range.reset_start = this.graphDataLimits.l;
            this.#graphFigure.x_range.reset_end = this.graphDataLimits.r;
            this.#graphFigure.y_range.reset_start = this.graphDataLimits.b;
            this.#graphFigure.y_range.reset_end = this.graphDataLimits.t;
         }

         for(let i =0; i< this.graphFitRenderers.length; i++)
            for (let j = 0; j < this.#graphFigure.legend.items.length; j++)
               if(this.#graphFigure.legend.items[j].renderers[0] === this.graphFitRenderers[i]) {
                  this.#graphFigure.legend.items[j].label =  (this.seriesNames[i] ? `${this.seriesNames[i]}: ` : '') +  (this.seriesErrors[i]?.length ? `${this.seriesErrors[i]}` : "Fit");
                  break;
               }
      }
   }

   createGraphFigure() {
      if (this.#graphFigure || !this.tableData || !this.container)
         return;

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

      const td = this.tableData;
      const xAxisColId = this.xAxisColId ?? this.xAxisDefaultColId;
      const yAxisColId = this.yAxisColId ?? this.yAxisDefaultColId;
      const yAxisErrorBarsColId = this.yAxisErrorBarsColId ?? this.yAxisErrorBarsDefaultColId;
      const title = this?.title ?? this?.metaFit?.name;

      if (!this.graphDataSources) {
         let figparams = {
            sizing_mode: "stretch_both",
            x_range: new Bokeh.Range1d({ start: 0, end: 1 }),
            y_range: new Bokeh.Range1d({ start: 0, end: 1 }),
            tools: ""
         };
         if (title) {
            figparams["title"] = title;
         }
         const fig = Bokeh.Plotting.figure(figparams);
         this.styleNoDataFigure(fig, "", "");
         fig.add_layout(new Bokeh.Label({
            x: 0.5, y: 0.5, x_units: "data", y_units: "data",
            text: this.error_message, //"no data",
            text_font_size: "12px", text_align: "center", text_color: color_text
         }));
         this.container.innerText = "";
         Bokeh.Plotting.show(fig, this.container);
         return;
      }

      const xrng = new Bokeh.Range1d({ start: this.graphDataLimits.l, end: this.graphDataLimits.r });
      const yrng = new Bokeh.Range1d({ start: this.graphDataLimits.b, end: this.graphDataLimits.t });

      let figparams = {
         x_range: xrng, y_range: yrng, x_axis_type: "log",
         tools: this.graphTools.filter(item => item !== "tap" && item !== "hover").join(","),
         sizing_mode: "stretch_both"
      };

      if (title) {
         figparams["title"] = title;
      }

      const xAxisIsLog = true;
      if (xAxisIsLog) {
         figparams["x_axis_type"] = "log";
      }

      const fig = Bokeh.Plotting.figure(figparams);
      this.styleGraphFigure(fig, td.colTitleAndUnit(xAxisColId), td.colTitleAndUnit(yAxisColId));
      fig.xgrid.minor_grid_line_alpha = 0;
      fig.ygrid.minor_grid_line_alpha = 0;

      let allRenderers = [];
      this.dataRenderers = [];
      this.whiskerLayouts = [];
      for (let i = 0; i < this.graphDataSources.length; i++) {
         const dataSource = this.graphDataSources[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,
            legend_label: this.legendRawGroupDataLabels[i]
         });

         allRenderers.push(dataRenderer);
         this.dataRenderers.push(dataRenderer);

         if (yAxisErrorBarsColId) {
            const whisker = new Bokeh.Whisker({
               base: { field: "x" }, upper: { field: "errhi" }, lower: { field: "errlo" },
               source: dataSource.raw
            });

            whisker.visible = this.errorBarsVisible;
            whisker.line_width = 2;
            whisker.line_alpha = 0.7;
            whisker.line_color = { field: "color" };
            whisker.upper_head.line_color = { field: "color" };
            whisker.lower_head.line_color = { field: "color" };
            fig.add_layout(whisker);
            this.whiskerLayouts.push(whisker);
         }

         const fitRenderer = fig.line({ field: "x" }, { field: "y" }, {
            source: dataSource.fit,
            line_width: 2, color: this.seriesColorsFit[i],
            legend_label: (this.seriesNames[i] ? `${this.seriesNames[i]}: ` : '') +  (this.seriesErrors[i]?.length ? `${this.seriesErrors[i]}` : "Fit")
         });
         this.graphFitRenderers[i] = fitRenderer;
         allRenderers.push(fitRenderer);

         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.graphTools.includes("hover")) {
         this.datahovertool = new Bokeh.HoverTool({
            renderers: allRenderers,
            tooltips: `@tooltip`
         });
         fig.add_tools(this.datahovertool);
      }

      if (this.graphTools.includes("tap")) {
         this.taptool = new Bokeh.TapTool({ renderers: this.dataRenderers, behavior: 'inspect' });
         this.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.tableRowSelection && Array.isArray(sel) && Array.isArray(data?.r)) {
                     this.rowSelection = [data.r[sel[0]]];
                  }
               }
            },
            code: "fn(cb_obj, cb_data);"
         });
         fig.add_tools(this.taptool);
      }

      fig.legend.visible = this.legendVisible;
      fig.legend.location = this.legendDefaultLocation;

      this.container.innerText = "";
      Bokeh.Plotting.show(this.#graphFigure = fig, this.container);
   }

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

   set fitEquationColId(val) {
      if (this.#fitEquationColId === val)
         return;
      this.#fitEquationColId = val;
      this.fitEquationOptionValueChanged.emit(this);
      this.update();
   }
}

/*___________________________________________________________________________*/
class LimGraphBarchart extends LimGraphBokeh {
   #graphDataSource
   #legendData

   #xAxisOptionValuesChanged
   #xAxisColId
   #xAxisDefaultFeature
   #xAxisAllowedFeatures
   #xAxisForbiddenFeatures
   #xAxisFeatureMap
   #xAxisOptionValueChanged

   #yAxisOptionValuesChanged
   #yAxisColIds
   #yAxisDefaultFeature
   #yAxisAllowedFeatures
   #yAxisForbiddenFeatures
   #yAxisFeatureMap
   #yAxisMinimum
   #yAxisMaximum
   #yAxisOptionValueChanged

   #autoScaleOptionValue
   #autoScaleOptionValueChanged

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

   constructor(...args) {
      super(...args)
      const [container, pars, ...remainingArgs] = args;
      this.#graphDataSource = null;
      this.#legendData = [];

      this.#xAxisColId = pars?.xAxisColId ? pars.xAxisColId : "";
      this.#xAxisOptionValuesChanged = new LimSignal(pars?.xAxisOptionValuesChanged ? [pars.xAxisOptionValuesChanged] : []);
      this.#xAxisOptionValueChanged = new LimSignal(pars?.xAxisOptionValueChanged ? [pars.xAxisOptionValueChanged] : []);
      this.#xAxisDefaultFeature = pars?.xAxisDefaultFeature ? pars.xAxisDefaultFeature : undefined;
      this.#xAxisAllowedFeatures = pars?.xAxisAllowedFeatures ? pars.xAxisAllowedFeatures : undefined;
      this.#xAxisForbiddenFeatures = pars?.xAxisForbiddenFeatures ? pars.xAxisForbiddenFeatures : undefined;
      this.#xAxisFeatureMap = new Map();

      this.#yAxisColIds = pars?.yAxisColIds ?? [];
      this.#yAxisOptionValuesChanged = new LimSignal(pars?.yAxisOptionValuesChanged ? [pars.yAxisOptionValuesChanged] : []);
      this.#yAxisOptionValueChanged = new LimSignal(pars?.yAxisOptionValueChanged ? [pars.yAxisOptionValueChanged] : []);
      this.#yAxisDefaultFeature = pars?.yAxisDefaultFeature ? pars.yAxisDefaultFeature : undefined;
      this.#yAxisAllowedFeatures = pars?.yAxisAllowedFeatures ? pars.yAxisAllowedFeatures : undefined;
      this.#yAxisForbiddenFeatures = pars?.yAxisForbiddenFeatures ? pars.yAxisForbiddenFeatures : undefined;
      this.#yAxisFeatureMap = new Map();
      this.#yAxisMinimum = LimGraphBokeh.tryNumber(pars?.yAxisMinimum);
      this.#yAxisMaximum = LimGraphBokeh.tryNumber(pars?.yAxisMaximum);

      this.barWidth = LimGraphBokeh.tryNumber(pars?.barWidth) ?? 0.9;
      this.seriesGrouped = pars?.seriesGrouped ?? false;
      this.seriesColorList = pars?.seriesColorList;
      this.xAxisMajorLabelOrientation = pars?.xAxisMajorLabelOrientation ?? "";
      this.xAxisRangePadding = LimGraphBokeh.tryNumber(pars?.xAxisRangePadding) ?? 0;
      this.xAxisGroupPadding = LimGraphBokeh.tryNumber(pars?.xAxisGroupPadding) ?? 1.4;
      this.xAxisSeriesTicks = pars?.xAxisSeriesTicks ?? true;


      this.defaultGraphTools = ["reset", "tap", "xpan", "xwheel_zoom"];

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

      if (pars?.xAxisOptionVisible ?? true) {
         this.optionList.set("xAxis", {
            type: "selection",
            title: "X axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_x.svg"
         });
      }

      if (pars?.yAxisOptionVisible ?? true) {
         this.optionList.set("yAxis", {
            type: "multi-selection",
            title: "Y axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_y.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"
         });
      }

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

   update() {
      if (!this.tableData)
         return;

      this.updateFeatureList();
      this.fetchAndFillGraphDataSource();
   }

   updateFeatureList() {
      if (!this.tableData)
         return;

      const td = this.tableData;
      this.#xAxisFeatureMap = this.makeFeatureMap(this.#xAxisAllowedFeatures, this.#xAxisForbiddenFeatures, (meta) => !meta?.jsonObject);

      this.#xAxisOptionValuesChanged.emit(this);

      if (![...this.#xAxisFeatureMap.keys()].includes(this.#xAxisColId) && this.#xAxisDefaultFeature) {
         this.#xAxisColId = td.colIdAt(td.matchColsFulltext(this.#xAxisDefaultFeature)[0] ?? -1);
      }

      if (![...this.#xAxisFeatureMap.keys()].includes(this.#xAxisColId)) {
         const vals = [...this.#xAxisFeatureMap.keys()];
         this.#xAxisColId = [vals.find(item => item === '_Well') ?? "", vals.find(item => item[0] != '_') ?? ""].find(item => item != "") ?? "";
      }

      this.#xAxisOptionValueChanged.emit(this);

      this.#yAxisFeatureMap = this.makeNumericFeatureMap(this.#yAxisAllowedFeatures, this.#yAxisForbiddenFeatures);
      this.#yAxisOptionValuesChanged.emit(this);

      this.#yAxisColIds = this.#yAxisColIds.filter(item => [...this.#yAxisFeatureMap.keys()].includes(item));
      if (0 === this.#yAxisColIds.length && this.#yAxisDefaultFeature) {
         if (Array.isArray(this.#yAxisDefaultFeature))
            this.#yAxisColIds = this.#yAxisDefaultFeature.map(item => td.colIdAt(td.matchColsFulltext(item)[0] ?? -1)).filter(item => item);
         else if (typeof this.#yAxisDefaultFeature === "string")
            this.#yAxisColIds = td.matchColsFulltext(this.#yAxisDefaultFeature).map(item => td.colIdAt(item)).filter(item => item);
      }

      if (0 === this.#yAxisColIds.length) {
         const vals = [...this.#yAxisFeatureMap.keys()];
         this.#yAxisColIds = [vals.find(item => item[0] != '_' && item != this.#xAxisColId, this) ?? ""];
      }

      this.#yAxisOptionValueChanged.emit(this);
   }

   get dataColumns() {
      let ret = this.tableData.systemColIdList;
      if (this.#xAxisColId)
         ret.push(this.#xAxisColId);
      return ret.concat(this.#yAxisColIds);
   }

   get rowFilter() {
      let ret = [];
      if (this.#xAxisColId)
         ret.push({ op: 'valid', col: this.#xAxisColId });
      return ret.concat(this.#yAxisColIds.map(item => ({ op: 'valid', col: item })));
   }

   fillGraphDataSource() {
      if (!this.tableData)
         return;

      this.#graphDataSource = new Bokeh.ColumnDataSource();

      const xColIndex = this.tableData.colIndexById(this.#xAxisColId);
      const yColIndexes = this.#yAxisColIds.map(item => this.tableData.colIndexById(item)).filter(item => 0 <= item);
      if (xColIndex < 0)
         return;

      const xdata = this.tableData.colDataAt(xColIndex);
      const categories = new Set(xdata.filter(item => item !== null && item !== ""));
      const filter = xdata.map(item => categories.has(item));

      let colors = null;
      if (Array.isArray(this.seriesColorList))
         colors = this.seriesColorList;
      else
         colors = Object.values(this.colorList(this.seriesColorList));

      let mult = 1;
      let data = { x: [], y: [], r: [], c: [] };
      const xx = xdata.filter((_, i) => filter[i]).map(item => `${item}`);
      const names = yColIndexes.map(item => this.tableData.colTitleAt(item));
      if (0 === yColIndexes.length) {
         data.x = xx;
         data.y = xx.map(item => 0);
      }
      else if (1 === yColIndexes.length) {
         data.x = xx;
         data.y = this.tableData.colDataAt(yColIndexes[0]).filter((_, i) => filter[i]);
         data.r = filter.map((val, index) => val ? index : -1).filter(item => (0 <= item));
      }
      else {
         const rrs = filter.map((val, index) => val ? index : -1).filter(item => (0 <= item));
         if (this.seriesGrouped) {
            mult = xx.length;
            for (let j = 0; j < yColIndexes.length; j++) {
               const yColIndex = yColIndexes[j];
               const yy = this.tableData.colDataAt(yColIndex).filter((_, i) => filter[i]);
               data.x.push(...xx.map(item => [names[j], item]));
               data.y.push(...yy);
               data.r.push(...rrs);
               if (colors) {
                  const col = colors[j % colors.length]
                  data.c.push(...xx.map(item => col));
               }
            }
         }
         else {
            const yys = yColIndexes.map(item => this.tableData.colDataAt(item).filter((_, i) => filter[i]));
            for (let i = 0; i < xx.length; i++) {
               for (let j = 0; j < yColIndexes.length; j++) {
                  data.x.push([xx[i], names[j]]);
                  data.y.push(yys[j][i]);
                  data.r.push(rrs[i]);
                  if (colors)
                     data.c.push(colors[j % colors.length]);
               }
            }
         }
      }

      if (0 === data.c.length)
         delete data.c;

      this.#legendData = names.map((item, i) => ({ label: item, index: i * mult }));
      this.#graphDataSource.data = data;
   }

   createGraphFigure() {
      if (!this.tableData || !this.container)
         return;

      const xColIndex = this.tableData.colIndexById(this.#xAxisColId);
      const yColIndexes = this.#yAxisColIds.map(item => this.tableData.colIndexById(item)).filter(item => 0 <= item);
      if (xColIndex < 0)
         return;

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

      let ymetas = [];
      let [ymin, ymax] = [0, 1];
      const xmeta = this.tableData.colMetadata(this.#xAxisColId);
      if (0 < this.#yAxisColIds.length) {
         ymetas = this.#yAxisColIds.map(item => this.tableData.colMetadata(item));
         const ydata = this.#graphDataSource.data.y;
         const globalRangeMinMax = {
            min: Math.min(...ymetas.filter(item => item.globalRange?.min).map(item => item.globalRange?.min), ...ydata),
            max: Math.max(...ymetas.filter(item => item.globalRange?.max).map(item => item.globalRange?.max), ...ydata)
         };
         [ymin, ymax] = LimGraphBokeh.calculateAxisRange(ydata, this.#autoScaleOptionValue, [this.#yAxisMinimum, this.#yAxisMaximum], globalRangeMinMax, [0, 0.05], 0.20);
      }

      let fill_color = color_graph_data;
      if (this.#graphDataSource.data.c)
         fill_color = { field: "c" };

      const categories = new Set(this.#graphDataSource.data.x);
      const keys = [...categories.keys()];
      const xrng = new Bokeh.FactorRange({ factors: keys, range_padding: this.xAxisRangePadding, group_padding: this.xAxisGroupPadding });
      const yrng = new Bokeh.Range1d({ start: 0, end: ymax });
      const 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"
      });

      this.styleGraphFigure(fig,
         xmeta.units ? `${xmeta.title} [${xmeta.units}]` : `${xmeta.title}`,
         1 === yColIndexes.length ? (ymetas[0].units ? `${ymetas[0].title} [${ymetas[0].units}]` : `${ymetas[0].title}`) : undefined
      );

      const defaultOrientation = 1 < yColIndexes.length && !this.seriesGrouped ? "vertical" : "horizontal";
      fig.xaxis.major_label_orientation = this.xAxisMajorLabelOrientation === "" ? defaultOrientation : (isNaN(this.xAxisMajorLabelOrientation) ? this.xAxisMajorLabelOrientation : this.xAxisMajorLabelOrientation * Math.PI / 180);
      fig.xgrid.grid_line_color = null;

      if (1 < yColIndexes.length && !this.xAxisSeriesTicks) {
         if (this.seriesGrouped) {
         }
         else {
            fig.xaxis.formatter = new Bokeh.PrintfTickFormatter({ format: "" });
            fig.xaxis.major_tick_out = 0;
            fig.xaxis.major_tick_in = 0;
         }
      }

      const renderer = fig.vbar({
         x: { field: "x" }, top: { field: "y" }, width: this.barWidth,
         source: this.#graphDataSource,
         fill_color: fill_color, fill_alpha: 0.8, line_color: null,
         selection_fill_color: fill_color, selection_fill_alpha: 0.8, selection_line_color: color_graph_highlight, selection_line_alpha: 1,
         nonselection_fill_color: fill_color, nonselection_fill_alpha: 0.8, nonselection_line_color: null,
      });

      if (this.graphTools.includes("tap")) {
         this.taptool = new Bokeh.TapTool({ renderers: [renderer], behavior: 'inspect' });
         this.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(this.taptool);
      }

      if (0 < this.#legendData.length && this.legendVisible) {
         if (this.legendOutsidePlacement) {
            const legend = new Bokeh.Legend();
            this.styleGraphLegend(legend);
            legend.location = this.legendLocation;
            legend.orientation = this.legendOrientation;
            legend.items = this.#legendData.map(item => new Bokeh.LegendItem({ label: item.label, renderers: [renderer], index: item.index }));
            fig.add_layout(legend, this.legendOutsidePlacement);
         }
         else {
            fig.legend.location = this.legendLocation;
            fig.legend.orientation = this.legendOrientation;
            fig.legend.items = this.#legendData.map(item => new Bokeh.LegendItem({ label: item.label, renderers: [renderer], index: item.index }));
         }
      }

      this.container.innerText = "";
      Bokeh.Plotting.show(fig, this.container);
   }

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

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

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

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

   set xAxisAllowedFeatures(val) {
      this.#xAxisAllowedFeatures = val;
      this.update();
   }

   get xAxisForbiddenFeatures() {
      return this.#xAxisForbiddenFeatures;
   }
   set xAxisForbiddenFeatures(val) {
      this.#xAxisForbiddenFeatures = val;
      this.update();
   }

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

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

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

   get yAxisAllowedFeatures() {
      return this.#yAxisAllowedFeatures;
   }
   set yAxisAllowedFeatures(val) {
      this.#yAxisAllowedFeatures = val;
      this.update();
   }

   get yAxisForbiddenFeatures() {
      return this.#yAxisForbiddenFeatures;
   }
   set yAxisForbiddenFeatures(val) {
      this.#yAxisForbiddenFeatures = val;
      this.update();
   }

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

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

   get xAxisOptionValues() {
      return [...this.#xAxisFeatureMap.entries()];
   }

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

   get xAxisOptionValue() {
      return this.xAxisColId;
   }

   set xAxisOptionValue(val) {
      this.xAxisColId = val;
   }

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

   get yAxisOptionValues() {
      return [...this.#yAxisFeatureMap.entries()];
   }

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

   get yAxisOptionValue() {
      return this.yAxisColIds;
   }

   set yAxisOptionValue(val) {
      this.yAxisColIds = val;
   }

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

   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
   }
}

class LimGraphBars extends LimGraphBokeh {
   #graphDataSource

   #yAxisOptionValuesChanged
   #yAxisColIds
   #yAxisDefaultFeature
   #yAxisAllowedFeatures
   #yAxisForbiddenFeatures
   #yAxisFeatureMap
   #yAxisMinimum
   #yAxisMaximum
   #yAxisOptionValueChanged

   #autoScaleOptionValue
   #autoScaleOptionValueChanged

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

   constructor(...args) {
      super(...args)
      const [container, pars, ...remainingArgs] = args;
      this.#graphDataSource = null;

      this.#yAxisColIds = pars?.yAxisColIds ?? null;
      this.#yAxisOptionValuesChanged = new LimSignal(pars?.yAxisOptionValuesChanged ? [pars.yAxisOptionValuesChanged] : []);
      this.#yAxisOptionValueChanged = new LimSignal(pars?.yAxisOptionValueChanged ? [pars.yAxisOptionValueChanged] : []);
      this.#yAxisDefaultFeature = pars?.yAxisDefaultFeature ? pars.yAxisDefaultFeature : undefined;
      this.#yAxisAllowedFeatures = pars?.yAxisAllowedFeatures ? pars.yAxisAllowedFeatures : undefined;
      this.#yAxisForbiddenFeatures = pars?.yAxisForbiddenFeatures ? pars.yAxisForbiddenFeatures : undefined;
      this.#yAxisFeatureMap = new Map();
      this.#yAxisMinimum = LimGraphBokeh.tryNumber(pars?.yAxisMinimum);
      this.#yAxisMaximum = LimGraphBokeh.tryNumber(pars?.yAxisMaximum);

      this.xAxisRangePadding = LimGraphBokeh.tryNumber(pars?.xAxisRangePadding) ?? 0;
      this.barWidth = LimGraphBokeh.tryNumber(pars?.barWidth) ?? 0.9;
      this.seriesColorList = pars?.seriesColorList;


      this.defaultGraphTools = ["reset"];
      if (!this.iconres)
         this.iconres = LimGraphBars.iconres;
      if (!this.name)
         this.name = LimGraphBars.name;

      this.optionList = new Map();
      if (pars?.yAxisOptionVisible ?? true) {
         this.optionList.set("yAxis", {
            type: "multi-selection",
            title: "Y axis feature",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_y.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"
         });
      }

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

   update() {
      if (!this.tableData)
         return;

      this.updateFeatureList();
      this.fetchAndFillGraphDataSource();
   }

   updateFeatureList() {
      if (!this.tableData)
         return;

      const td = this.tableData;
      this.#yAxisFeatureMap = this.makeNumericFeatureMap(this.#yAxisAllowedFeatures, this.#yAxisForbiddenFeatures);

      this.#yAxisOptionValuesChanged.emit(this);
      if (this.#yAxisColIds)
         this.#yAxisColIds = this.#yAxisColIds.filter(item => [...this.#yAxisFeatureMap.keys()].includes(item));


      if (null === this.#yAxisColIds && this.#yAxisDefaultFeature) {
         if (Array.isArray(this.#yAxisDefaultFeature))
            this.#yAxisColIds = this.#yAxisDefaultFeature.map(item => td.colIdAt(td.matchColsFulltext(item)[0] ?? -1)).filter(item => item);
         else if (typeof this.#yAxisDefaultFeature === "string")
            this.#yAxisColIds = [td.colIdAt(td.matchColsFulltext(this.#yAxisDefaultFeature)[0] ?? -1)];
      }

      if (null === this.#yAxisColIds) {
         const vals = [...this.#yAxisFeatureMap.keys()];
         this.#yAxisColIds = [vals.find(item => item[0] != '_') ?? ""];
      }

      this.#yAxisOptionValueChanged.emit(this);
   }

   get dataColumns() {
      let ret = this.tableData.systemColIdList;
      return ret.concat(this.#yAxisColIds);
   }

   get rowFilter() {
      return this.#yAxisColIds.map(item => ({ op: 'valid', col: item }));
   }

   fillGraphDataSource() {
      if (!this.tableData)
         return;

      let colors = null;
      if (Array.isArray(this.seriesColorList))
         colors = this.seriesColorList;
      else
         colors = Object.values(this.colorList(this.seriesColorList));

      this.#graphDataSource = null;
      if (Array.isArray(this.#yAxisColIds) && this.#yAxisColIds.length) {
         this.#graphDataSource = new Bokeh.ColumnDataSource();
         let data = { i: [], x: [], y: [], c: [], t: [] };
         const yColIndexes = this.#yAxisColIds.map(item => this.tableData.colIndexById(item)).filter(item => 0 <= item);
         for (let i = 0; i < yColIndexes.length; i++) {
            const j = yColIndexes[i];
            const x = this.tableData.colTitleAt(j);
            const y = this.tableData.dataColumnList?.[j]?.[0];
            const print = LimTableData.makePrintFunction(this.tableData.colMetadataAt(j));
            data.i.push(i);
            data.x.push(x);
            data.y.push(y);
            data.c.push(colors[i % colors.length]);
            data.t.push(print(y));
            this.#graphDataSource.data = data;
         }
      }
   }

   calcAxisLimit(ydata, ymetas) {
      if (this.#autoScaleOptionValue) {
         let [ymin, ymax] = [Math.min(...ydata), Math.max(...ydata)];
         const margin = ((ymax != ymin) ? (ymax - ymin) : ymax) * 0.1;
         return [Math.max(0, ymin - margin), ymax + margin];
      }
      else {
         const ymin = this.#yAxisMinimum ?? Math.min(...ymetas.filter(item => typeof item?.globalRange?.min == "number").map(item => item?.globalRange?.min), ...ydata);
         const ymax = this.#yAxisMaximum ?? Math.max(...ymetas.filter(item => typeof item?.globalRange?.max == "number").map(item => item?.globalRange?.max), ...ydata);
         const margin = ((ymax != ymin) ? (ymax - ymin) : ymax) * 0.1;
         return [this.#yAxisMinimum ?? Math.max(0, ymin - margin), this.#yAxisMaximum ?? ymax + margin];
      }
   }

   createGraphFigure() {
      if (!this.tableData || !this.container)
         return;

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

      if (!this.#graphDataSource) {
         let figparams = { sizing_mode: "stretch_both", x_range: new Bokeh.Range1d({ start: 0, end: 1 }), y_range: new Bokeh.Range1d({ start: 0, end: 1 }) };
         if (this.title)
            figparams["title"] = this.title;
         const fig = Bokeh.Plotting.figure(figparams);
         this.styleNoDataFigure(fig);
         fig.add_layout(new Bokeh.Label({
            x: 0.5, y: 0.5, x_units: "data", y_units: "data",
            text: "Graph is not available",
            text_font_size: "12px", text_align: "center", text_color: color_text
         }));
         this.container.innerText = "";
         Bokeh.Plotting.show(fig, this.container);
         return;
      }

      let ymetas = [];
      let [ymin, ymax] = [0, 1];
      if (0 < this.#yAxisColIds.length) {
         ymetas = this.#yAxisColIds.map(item => this.tableData.colMetadata(item));
         [ymin, ymax] = this.calcAxisLimit(this.#graphDataSource.data.y, ymetas);
      }

      let fill_color = color_graph_data;
      if (this.#graphDataSource.data.c)
         fill_color = { field: "c" };

      const xrng = new Bokeh.FactorRange({ factors: this.#graphDataSource.data.x, range_padding: this.xAxisRangePadding });
      const yrng = new Bokeh.Range1d({ start: ymin, end: ymax });
      const fig = Bokeh.Plotting.figure({
         title: this.title,
         x_range: xrng, y_range: yrng,
         tools: this.graphTools.filter(item => item !== "hover").join(","),
         sizing_mode: "stretch_both"
      });

      this.styleGraphFigure(fig, undefined, undefined);

      fig.xgrid.grid_line_color = null;
      fig.xaxis.major_tick_out = 0;
      fig.xaxis.major_tick_in = 0;

      const renderer = fig.vbar({
         x: { field: "x" }, top: { field: "y" }, width: this.barWidth,
         source: this.#graphDataSource,
         fill_color: fill_color, fill_alpha: 0.8, line_color: null,
         selection_fill_color: fill_color, selection_fill_alpha: 0.8, selection_line_color: color_graph_highlight, selection_line_alpha: 1,
         nonselection_fill_color: fill_color, nonselection_fill_alpha: 0.8, nonselection_line_color: null,
      });

      const labels = new Bokeh.LabelSet({
         x: { field: 'x' }, y: { field: 'y' }, text: { field: 't' }, x_offset: 0, y_offset: 5,
         text_color: fill_color, text_font: this.fontFamily, text_font_size: "12px", text_align: "center",
         source: this.#graphDataSource
      });

      fig.add_layout(labels);

      this.container.innerText = "";
      Bokeh.Plotting.show(fig, this.container);
   }

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

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

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

   get yAxisAllowedFeatures() {
      return this.#yAxisAllowedFeatures;
   }
   set yAxisAllowedFeatures(val) {
      this.#yAxisAllowedFeatures = val;
      this.update();
   }

   get yAxisForbiddenFeatures() {
      return this.#yAxisForbiddenFeatures;
   }
   set yAxisForbiddenFeatures(val) {
      this.#yAxisForbiddenFeatures = val;
      this.update();
   }

   get state() {
      return {
         yAxisColIds: this.yAxisColIds
      }
   }

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

   get yAxisOptionValues() {
      return [...this.#yAxisFeatureMap.entries()];
   }

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

   get yAxisOptionValue() {
      return this.yAxisColIds;
   }

   set yAxisOptionValue(val) {
      this.yAxisColIds = val;
   }

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

   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
   }
}

class LimGraphContainer extends HTMLElement {
   #bokehInstance
   #bokehClass
   #pars

   constructor() {
      super();
   }

   initialize(bokehClass, ...pars) {
      this.className = "lim-container";
      this.#bokehClass = bokehClass;
      this.#pars = pars;
   }

   connectedCallback() {
      this.#bokehInstance = new this.#bokehClass(this, ...this.#pars);
      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.#pars?.title ?? "";
   }

   get name() {
      return this.#pars?.name || this.#bokehClass.name;
   }

   get iconres() {
      return this.#pars?.title || this.#bokehClass.iconres;
   }
};

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

LimClassFactory.registerConstructor("LimGraphHistogram", (...pars) => createGraphContainer(LimGraphHistogram, ...pars));
LimClassFactory.registerConstructor("LimGraphScatterplot", (...pars) => createGraphContainer(LimGraphScatterplot, ...pars));
LimClassFactory.registerConstructor("LimGraphFittedData", (...pars) => createGraphContainer(LimGraphFittedData, ...pars));
LimClassFactory.registerConstructor("LimGraphBarchart", (...pars) => createGraphContainer(LimGraphBarchart, ...pars));
LimClassFactory.registerConstructor("LimGraphBars", (...pars) => createGraphContainer(LimGraphBars, ...pars));

