diff --git a/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.py b/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.py index 9cf64392..004e94ec 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.py +++ b/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.py @@ -17,7 +17,7 @@ class CDSJoin(ColumnarDataSource): on_left = List(String) on_right = List(String) prefix_left = String(help="Prefix to use for columns in the left column data source") - prefix_right = String(help="Prefix to use for columns in the left column data source") + prefix_right = String(help="Prefix to use for columns in the right column data source") how = String(default="inner") tolerance = Float(default=1e-5) print("Import ", __implementation__) diff --git a/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.ts b/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.ts index 2784fdf3..fdc71b95 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.ts +++ b/RootInteractive/InteractiveDrawing/bokeh/CDSJoin.ts @@ -43,9 +43,12 @@ export class CDSJoin extends ColumnarDataSource { _indices_left: number[] | null _indices_right: number[] | null + cached_columns: Set + initialize(){ super.initialize() this.data = {} + this.cached_columns = new Set() this.compute_indices() } @@ -73,6 +76,9 @@ export class CDSJoin extends ColumnarDataSource { if (on_right.length === 0){ this._indices_left = null this._indices_right = null + this.cached_columns.clear() + change.emit() + return } } else if(on_left.length === 1 && on_right.length === 0){ this._indices_left = null @@ -176,30 +182,31 @@ export class CDSJoin extends ColumnarDataSource { this._indices_left = indices_left this._indices_right = indices_right } - for (const key of left.columns()) { - const col = left.get_array(key) - if(col !== null) this.data[key] = this.join_column(col, this._indices_left) - } - for (const key of right.columns()) { - const col = right.get_array(key) - if(col !== null) this.data[key] = this.join_column(col, this._indices_right) - } // selected.indices = this.source.selected.indices + this.cached_columns.clear() change.emit() } get_column(key: string){ - const {left, right, data} = this - if (data[key] != null) return data[key] + const {left, right, data, prefix_left, prefix_right} = this + if (this.cached_columns.has(key)) return data[key] let column = null - try { - column = left.get_column(key) - } - catch { - column = right.get_column(key) - } - if (column == null){ - column = right.get_column(key) + if (prefix_left && key.startsWith(prefix_left)){ + let key_new = key.replace(prefix_left,"") + column = left.get_column(key_new) + } else if(prefix_right && key.startsWith(prefix_right)){ + let key_new = key.replace(prefix_right,"") + column = right.get_column(key_new) + } else { + try { + column = left.get_column(key) + } + catch { + column = right.get_column(key) + } + if (column == null){ + column = right.get_column(key) + } } if(column != null) { if (!Array.isArray(column)){ @@ -208,11 +215,26 @@ export class CDSJoin extends ColumnarDataSource { data[key] = this.join_column(column, this._indices_right) } } + this.cached_columns.add(key) return data[key] } get_length(){ - if (this._indices_left == null) return 0 + if (this._indices_left == null){ + if(this._indices_right == null){ + let l = this.left.get_length() + if(l == null){ + return this.right.get_length() + } + let r = this.right.get_length() + if(r == null){ + return l + } + return Math.min(l, r) + } else { + return this._indices_right.length + } + } return this._indices_left.length } diff --git a/RootInteractive/InteractiveDrawing/bokeh/ConcatenatedString.ts b/RootInteractive/InteractiveDrawing/bokeh/ConcatenatedString.ts index 0c8e7b74..9b75ece0 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/ConcatenatedString.ts +++ b/RootInteractive/InteractiveDrawing/bokeh/ConcatenatedString.ts @@ -42,6 +42,7 @@ export class ConcatenatedString extends Model { compute(): string{ if (this.#is_dirty){ this.#value = String.prototype.concat(...this.components) + this.#is_dirty = false } return this.#value } diff --git a/RootInteractive/InteractiveDrawing/bokeh/DownsamplerCDS.ts b/RootInteractive/InteractiveDrawing/bokeh/DownsamplerCDS.ts index 70f721a6..750209e3 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/DownsamplerCDS.ts +++ b/RootInteractive/InteractiveDrawing/bokeh/DownsamplerCDS.ts @@ -56,7 +56,7 @@ export class DownsamplerCDS extends ColumnDataSource { this.low = 0 this.high = -1 this._needs_update = true - this._is_trivial = false + this._is_trivial = this.nPoints < 0 && this.filter == null this._cached_columns = new Set() } @@ -86,8 +86,10 @@ export class DownsamplerCDS extends ColumnDataSource { update(){ const {source, nPoints, selected, filter, _indices} = this const l = source.length + console.log(this.name) if(filter == null && (nPoints < 0 || nPoints >= l)){ this._is_trivial = true + this._needs_update = false return } if(nPoints < 0 || nPoints >= l){ @@ -226,6 +228,9 @@ export class DownsamplerCDS extends ColumnDataSource { } } - //Needed because of typescript, is supposed to be a nop - on_visible_change(){} + on_visible_change(){ + if(this.watched && this._needs_update){ + this.change.emit() + } + } } diff --git a/RootInteractive/InteractiveDrawing/bokeh/LazyTabs.ts b/RootInteractive/InteractiveDrawing/bokeh/LazyTabs.ts index 209d19ab..ff4b231e 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/LazyTabs.ts +++ b/RootInteractive/InteractiveDrawing/bokeh/LazyTabs.ts @@ -62,11 +62,13 @@ export namespace LazyTabs { const {active, _last_index} = this const old_renderers = this.renderers[_last_index] for(let i=0; i= minEntries"}], + [["bin_center_0"], ["std"], { "source":"histoXYData_1","errY":"std/sqrt(entries)","filter":"entries >= minEntries"}], + # histo XY weights + [["bin_center_0"], ["mean","quantile_1"], { "source":"histoXYData_1","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"histoXYData_1","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["bin_count"], { "source":"histoXYData", "colorZvar": "bin_center_1"}], + [["bin_center_0"], ["mean","quantile_1"], { "source":"projXYRef_1","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"projXYRef_1","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["histoXYRef"], { "source":"histoXYData", "colorZvar": "bin_center_1"}], + [["bin_center_0"], ["diffFuncWeights(histoXYData_1.mean, projXYRef_1.mean)", "diffFuncWeights(histoXYData_1.quantile_1, projXYRef_1.quantile_1)"], { "source":"histoXYData_1_join","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleWeightsMeanMedian,"filter":"histoXYData_1.entries >= minEntries and projXYRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(histoXYData_1.std, projXYRef_1.std )"], { "source":"histoXYData_1_join","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleWeightsStd,"filter":"histoXYData_1.entries >= minEntries and projXYRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(bin_count, histoXYRef)"], { "source":"histoXYData", "colorZvar": "bin_center_1","xAxisTitle":"{varX}","yAxisTitle":"{varY} - {varY} (ref)"}], + # histoXYNorm + [[("bin_bottom_0", "bin_top_0")], [("bin_bottom_1", "bin_top_1")], {"colorZvar": "bin_count", "source":"histoXYNormData","yAxisTitle":yAxisTitleNorm}], + [["bin_center_1"], ["bin_count"], { "source":"histoXYNormData", "colorZvar": "bin_center_0","yAxisTitle":yAxisTitleNorm}], + [["bin_center_0"], ["mean","quantile_1",], { "source":"histoXYNormData_1","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries"}], + [["bin_center_0"], ["std"], { "source":"histoXYNormData_1","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries"}], + # histoXYNorm weights + [["bin_center_0"], ["mean","quantile_1"], { "source":"histoXYNormData_1","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"histoXYNormData_1","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["bin_count"], { "source":"histoXYNormData", "colorZvar": "bin_center_1","yAxisTitle":yAxisTitleNorm}], + [["bin_center_0"], ["mean","quantile_1"], { "source":"projXYNormRef_1","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm+" (ref)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"projXYNormRef_1","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm+" (ref)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["histoXYRef"], { "source":"histoXYNormData", "colorZvar": "bin_center_1","yAxisTitle":yAxisTitleNorm+" (ref)"}], + [["bin_center_0"], ["diffFuncWeights(histoXYNormData_1.mean, projXYNormRef_1.mean)", "diffFuncWeights(histoXYNormData_1.quantile_1, projXYNormRef_1.quantile_1)"], { "source":"histoXYNormData_1_join","xAxisTitle":"{varX}", "yAxisTitle":yAxisTitleNormWeightsMeanMedian,"filter":"histoXYNormData_1.entries >= minEntries and projXYNormRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(histoXYNormData_1.std, projXYNormRef_1.std)"], { "source":"histoXYNormData_1_join","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleNormWeightsStd,"filter":"histoXYNormData_1.entries >= minEntries and projXYNormRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(bin_count, histoXYRef)"], { "source":"histoXYNormData", "colorZvar": "bin_center_1","xAxisTitle":"{varX}","yAxisTitle":f"{yAxisTitleNorm} - {yAxisTitleNorm} (ref)"}], + # histoXYZ + [["bin_center_0"], ["mean"], { "source":"histoXYZData_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","filter":"entries >= minEntries"}], + [["bin_center_0"], ["entries"], { "source":"histoXYZData_1","colorZvar":"bin_center_2","errY":"2*std/sqrt(entries)","filter":"entries >= minEntries"}], + [["bin_center_0"], ["quantile_1"], { "source":"histoXYZData_1","colorZvar":"bin_center_2","errY":"3*std/sqrt(entries)","filter":"entries >= minEntries"}], + [["bin_center_0"], ["std"], { "source":"histoXYZData_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","filter":"entries >= minEntries"}], + # histoXYZ weights + [["bin_center_0"], ["mean"], { "source":"histoXYZData_1", "colorZvar": "bin_center_2","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["quantile_1"], { "source":"histoXYZData_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"histoXYZData_1","colorZvar":"bin_center_2","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["mean"], { "source":"projXYZRef_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["quantile_1"], { "source":"projXYZRef_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"projXYZRef_1","colorZvar":"bin_center_2","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["diffFuncWeights(histoXYZData_1.mean, projXYZRef_1.mean)"], { "source":"histoXYZData_1_join", "colorZvar": "bin_center_2","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleWeightsMean,"colorAxisTitle":"{varZ}","filter":"histoXYZData_1.entries >= minEntries and projXYZRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(histoXYZData_1.quantile_1, projXYZRef_1.quantile_1)"], { "source":"histoXYZData_1_join", "colorZvar": "bin_center_2","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleWeightsMedian,"colorAxisTitle":"{varZ}","filter":"histoXYZData_1.entries >= minEntries and projXYZRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(histoXYZData_1.std, projXYZRef_1.std)"], { "source":"histoXYZData_1_join", "colorZvar": "bin_center_2","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleWeightsStd,"colorAxisTitle":"{varZ}"}], + # histoXYNormZ + [["bin_center_0"], ["mean"], { "source":"histoXYNormZData_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries"}], + [["bin_center_0"], ["entries"], { "source":"histoXYNormZData_1","colorZvar":"bin_center_2","errY":"2*std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries"}], + [["bin_center_0"], ["quantile_1"], { "source":"histoXYNormZData_1","colorZvar":"bin_center_2","errY":"3*std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries"}], + [["bin_center_0"], ["std"], { "source":"histoXYNormZData_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries"}], + # histoXYNormZ weights + [["bin_center_0"], ["mean"], { "source":"histoXYNormZData_1", "colorZvar": "bin_center_2","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["quantile_1"], { "source":"histoXYNormZData_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"histoXYNormZData_1","colorZvar":"bin_center_2","yAxisTitle":yAxisTitleNorm,"filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["mean"], { "source":"projXYNormZRef_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm+" (ref)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["quantile_1"], { "source":"projXYNormZRef_1","colorZvar":"bin_center_2","errY":"std/sqrt(entries)","yAxisTitle":yAxisTitleNorm+" (ref)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["std"], { "source":"projXYNormZRef_1","colorZvar":"bin_center_2","yAxisTitle":yAxisTitleNorm+" (ref)","filter":"entries >= minEntries and True"}], + [["bin_center_0"], ["diffFuncWeights(histoXYNormZData_1.mean, projXYNormZRef_1.mean)"], { "source":"histoXYNormZData_1_join", "colorZvar": "bin_center_2","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleNormWeightsMean,"colorAxisTitle":"{varZ}","filter":"histoXYNormZData_1.entries >= minEntries and projXYNormZRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(histoXYNormZData_1.quantile_1, projXYNormZRef_1.quantile_1)"], { "source":"histoXYNormZData_1_join", "colorZvar": "bin_center_2","xAxisTitle":"{varX}","yAxisTitle":yAxisTitleNormWeightsMedian,"colorAxisTitle":"{varZ}","filter":"histoXYNormZData_1.entries >= minEntries and projXYNormZRef_1.entries >= minEntries"}], + [["bin_center_0"], ["diffFuncWeights(histoXYNormZData_1.std, projXYNormZRef_1.std)"], { "source":"histoXYNormZData_1_join", "colorZvar": "bin_center_2","xAxisTitle":"{varX}","colorAxisTitle":"{varZ}","yAxisTitle":yAxisTitleNormWeightsStd}], + # histoXYZNormMedian + [[("bin_bottom_0", "bin_top_0")], [("bin_bottom_1", "bin_top_1")], {"colorZvar": "quantile_1", "source":"histoXYZData_2"}], + [[("bin_bottom_0", "bin_top_0")], [("bin_bottom_1", "bin_top_1")], {"colorZvar": "quantile_1", "source":"histoXYZNormData_2"}], + # histoXYZNormMean + [[("bin_bottom_0", "bin_top_0")], [("bin_bottom_1", "bin_top_1")], {"colorZvar": "mean", "source":"histoXYZData_2"}], + [[("bin_bottom_0", "bin_top_0")], [("bin_bottom_1", "bin_top_1")], {"colorZvar": "mean", "source":"histoXYZNormData_2"}], + # + ['descriptionTable', {"name":"description"}], + ['selectionTable', {"name":"selection"}], + [['bin_center_0'], ['bin_count'], {"source":"histoXYData", "name":"tex_hack", "xAxisTitle": "$$X_0$$"}], + figureGlobalOption + ] + figureLayoutDesc={ + "histoXY":[[0,1],[2,3], {"plot_height":250}], + "histoXYWeight":[[4,5,6],[7,8,9], [10,11,12],{"plot_height":260}], + "histoXYNorm":[[13,14],[15,16],{"plot_height":250}], + "histoXYNormWeight":[[17,18,19],[20,21,22], [23,24,25],{"plot_height":260}], + "histoXYZ":[[26,27],[28,29],{"plot_height":250}], + "histoXYZWeight":[[30,31,32],[33,34,35], [36,37,38],{"plot_height":260}], + "histoXYNormZ":[[39,40],[41,42],{"plot_height":250}], + "histoXYNormZWeight":[[43,44,45],[46,47,48], [49,50,51],{"plot_height":260}], + "histoXYNormZMedian":[[52,53],{"plot_height":350}], + "histoXYNormZMean":[[54,55],{"plot_height":350}], + } + figureLayoutDesc["selection"] = ["selection", {'plot_height': 200, 'sizing_mode': 'scale_width'}] + figureLayoutDesc["description"] = ["description", {'plot_height': 200, 'sizing_mode': 'scale_width'}] + figureLayoutDesc["ignoreme"] = ["tex_hack", {'plot_height': 200, 'sizing_mode': 'scale_width'}] + + print("Default RootInteractive variables are defined.") + return aliasArray, transformArray, variables, parameterArray, widgetParams, widgetLayoutDesc, histoArray, figureArray, figureLayoutDesc + def getDefaultVarsDiff(*args, **kwargs): return getDefaultVars("diff", *args, **kwargs) diff --git a/RootInteractive/InteractiveDrawing/bokeh/bokehTools.py b/RootInteractive/InteractiveDrawing/bokeh/bokehTools.py index 2b91d85d..fb2cc9a2 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/bokehTools.py +++ b/RootInteractive/InteractiveDrawing/bokeh/bokehTools.py @@ -59,7 +59,7 @@ ("count", "@bin_count") ] -BOKEH_DRAW_ARRAY_VAR_NAMES = ["X", "Y", "varZ", "colorZvar", "marker_field", "legend_field", "errX", "errY"] +BOKEH_DRAW_ARRAY_VAR_NAMES = ["X", "Y", "varZ", "colorZvar", "marker_field", "legend_field", "errX", "errY", "filter"] ALLOWED_WIDGET_TYPES = ["slider", "range", "select", "multiSelect", "toggle", "multiSelectBitmask", "spinner", "spinnerRange"] @@ -67,6 +67,8 @@ RE_VALID_NAME = re.compile(r"^[a-zA-Z_$][0-9a-zA-Z_$]*$") +RE_MATHFORMULA = re.compile(r"\$\$|\\\(|\\\[") + IS_PANDAS_1 = pd.__version__.split('.')[0] == '1' def mergeFigureArrays(figureArrayOld, figureArrayNew): @@ -351,7 +353,6 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr "markers": bokehMarkers, "colors": 'Category10', "rescaleColorMapper": False, - "filter": '', 'doDraw': False, "legend_field": None, "legendTitle": None, @@ -468,12 +469,12 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr result = evaluator.visit(exprTree.body) if result["type"] == "javascript": func = "return "+result["implementation"] - fields = list(evaluator.aliasDependencies) + fields = list(evaluator.aliasDependencies.values()) parameters = [i for i in evaluator.paramDependencies if "options" not in paramDict[i]] variablesParam = [i for i in evaluator.paramDependencies if "options" in paramDict[i]] customJsArgList = {i:paramDict[i]["value"] for i in evaluator.paramDependencies} nvars_local = len(fields) - variablesAlias = fields.copy() + variablesAlias = list(evaluator.aliasDependencies.keys()) for j in variablesParam: if "subscribed_events" not in paramDict[j]: paramDict[j]["subscribed_events"] = [] @@ -825,12 +826,16 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr varNameX = variablesLocal[0][0]["name"] varNameY = variablesLocal[1][0]["name"] varNameZ = variablesLocal[2][0]["name"] + if variablesLocal[8] is not None: + varNameFilter = variablesLocal[8][0]["name"] + else: + varNameFilter = None if variablesLocal[3] is not None: varNameColor = variablesLocal[3][0]["name"] else: varNameColor = None options3D = {"width": "99%", "height": "99%"} - cds_used = makeCdsSel(cdsDict, paramDict, cds_name) + cds_used = makeCdsSel(cdsDict, paramDict, cds_name, varNameFilter) plotI = BokehVisJSGraph3D( data_source=cds_used, x=varNameX, y=varNameY, z=varNameZ, style=varNameColor, options3D=options3D) plotArray.append(plotI) @@ -840,7 +845,8 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr else: figureI = figure(width=options.get("plot_width", 600), height=options.get("plot_height", 400), tools=options['tools'], x_axis_type=options['x_axis_type'], - y_axis_type=options['y_axis_type'], sizing_mode=options.get("sizing_mode", "stretch_width")) + y_axis_type=options['y_axis_type'], sizing_mode=options.get("sizing_mode", "stretch_width"), + title=r"\[x\pi\]") figureI.xaxis.axis_label_text_font_size = options["axis_label_text_font_size"] figureI.xaxis.major_label_text_font_size = options["major_label_text_font_size"] figureI.yaxis.axis_label_text_font_size = options["axis_label_text_font_size"] @@ -867,8 +873,13 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr variables_dict[axis_name] = variables_dict[axis_name][i % len(variables_dict[axis_name])] cds_name = cds_names[i] cds_used = None + varFilter = variables_dict["filter"] + if varFilter is None: + varNameFilter = None + else: + varNameFilter = varFilter["name"] if cds_name != "$IGNORE": - cds_used = makeCdsSel(cdsDict, paramDict, cds_name) + cds_used = makeCdsSel(cdsDict, paramDict, cds_name, varNameFilter) varColor = variables_dict["colorZvar"] if varColor is not None: if mapperC is not None: @@ -890,8 +901,8 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr colorMapperDict[varColor["name"]] = mapperC # HACK for projections - should probably just remove the rows as there's no issue with joins at all if cdsDict[cds_name]["type"] == "projection" and not rescaleColorMapper and varColor["name"].split('_')[0] == 'bin': - makeCdsSel(cdsDict, paramDict, cds_name) - cdsDict[cds_name]["cdsSel"].js_on_change('change', CustomJS(code=""" + makeCdsSel(cdsDict, paramDict, cds_name, varNameFilter) + cdsDict[cds_name]["cdsSel"][varNameFilter].js_on_change('change', CustomJS(code=""" const col = this.get_column(field) const isOK = this.get_column("isOK") const low = col.map((x,i) => isOK[i] ? col[i] : Infinity).reduce((acc, cur)=>Math.min(acc,cur), Infinity); @@ -959,7 +970,7 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr if isinstance(varY, str) and varY in cdsDict and cdsDict[varY]["type"] in ["histogram", "histo2d"]: histoHandle = cdsDict[varY] - makeCdsSel(cdsDict, paramDict, varY) + makeCdsSel(cdsDict, paramDict, varY, varNameFilter) if histoHandle["type"] == "histogram": colorHisto = colorAll[max(length, 4)][i] x_label = f"{{{histoHandle['variables'][0]}}}" @@ -1217,16 +1228,19 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr """)]) cdsFull.columnDependencies = list(aliasArrayLocal) - for iCds, widgets in widgetDict.items(): - widgetList = widgets["widgetList"] - cdsOrig = cdsDict[iCds]["cdsOrig"] - cdsFull = cdsDict[iCds]["cdsFull"] - cdsSel = cdsDict[iCds].get("cdsSel", None) + for iName, iCds in cdsDict.items(): + if "cdsFull" not in iCds: + continue + widgets = widgetDict.get(iName, {}) + widgetList = widgets.get("widgetList", []) + cdsOrig = iCds["cdsOrig"] + cdsFull = iCds["cdsFull"] + cdsSel = iCds.get("cdsSel", None) histoList = [] for cdsKey, cdsValue in cdsDict.items(): if cdsKey not in memoized_columns or "cdsOrig" not in cdsValue: continue - if cdsValue["type"] in ["histogram", "histo2d", "histoNd"] and cdsValue.get("source", None) == iCds: + if cdsValue["type"] in ["histogram", "histo2d", "histoNd"] and cdsValue.get("source", None) == iName: if isinstance(cdsValue["cdsOrig"], CDSStack): for i in cdsValue["cdsOrig"].sources: histoList.append(i) @@ -1236,13 +1250,21 @@ def bokehDrawArray(dataFrame, query, figureArray, histogramArray=[], parameterAr for iWidget in widgetList: if "filter" in iWidget: field = iWidget["filter"].field - if memoized_columns[iCds][field]["type"] in ["alias", "expr"]: + if memoized_columns[iName][field]["type"] in ["alias", "expr"]: iWidget["filter"].source = cdsFull else: iWidget["filter"].source = cdsOrig intersectionFilter.filters.append(iWidget["filter"]) - if cdsSel is not None and len(intersectionFilter.filters)>0: - cdsSel.filter = intersectionFilter + if cdsSel is not None: + for iFilter, iCdsSel in cdsSel.items(): + filterNew = iCds.get("filters", {}).get(iFilter, None) + if filterNew is not None: + if len(intersectionFilter.filters)>0: + iCdsSel.filter = LazyIntersectionFilter(filters=[intersectionFilter, filterNew]) + else: + iCdsSel.filter = filterNew + elif len(intersectionFilter.filters)>0: + iCdsSel.filter = intersectionFilter if len(intersectionFilter.filters)>0: for i in histoList: i.filter = intersectionFilter @@ -1303,7 +1325,7 @@ def addHisto2dGlyph(fig, histoHandle, marker, options): visualization_type = "heatmap" if "visualization_type" in options: visualization_type = options["visualization_type"] - cdsHisto = histoHandle["cdsSel"] + cdsHisto = histoHandle["cdsSel"][None] tooltips = None if "tooltips" in histoHandle: @@ -1339,7 +1361,7 @@ def addHisto2dGlyph(fig, histoHandle, marker, options): def addHistogramGlyph(fig, histoHandle, marker, colorHisto, size, options): - cdsHisto = histoHandle["cdsSel"] + cdsHisto = histoHandle["cdsSel"][None] if 'color' in options: colorHisto = options['color'] tooltips = None @@ -2052,7 +2074,7 @@ def getOrMakeCdsOrig(cdsDict: dict, paramDict: dict, key: str): how = iSource.get("how", "inner") sourceLeft = getOrMakeCdsFull(cdsDict, paramDict, left) sourceRight = getOrMakeCdsFull(cdsDict, paramDict, right) - iSource["cdsOrig"] = CDSJoin(left=sourceLeft, right=sourceRight, prefix_left=left or "cdsFull", prefix_right=right or "csFull", on_left=on_left, on_right=on_right, how=how, name=cdsName) + iSource["cdsOrig"] = CDSJoin(left=sourceLeft, right=sourceRight, prefix_left=(left+'.') if left else "cdsFull.", prefix_right=(right+'.') if right else "csFull.", on_left=on_left, on_right=on_right, how=how, name=cdsName) elif cdsType == "histogram": iSource = iCds weights = iSource.get("weights", None) @@ -2133,6 +2155,7 @@ def getOrMakeCdsFull(cdsDict: dict, paramDict: dict, key: str): def makeAxisLabelFromTemplate(template:str, paramDict:dict, meta: dict): components = re.split(RE_CURLY_BRACE, template) label = ConcatenatedString() + has_tex= True for i in range(1, len(components), 2): if components[i] in paramDict: if "options" in paramDict[components[i]]: @@ -2144,15 +2167,21 @@ def makeAxisLabelFromTemplate(template:str, paramDict:dict, meta: dict): label.properties.components.change.emit(); label.change.emit(); """)]) + for iOption in options.keys(): + m = re.match(RE_MATHFORMULA, iOption) + has_tex = has_tex or m is not None else: paramDict[components[i]]["subscribed_events"].append(["change", CustomJS(args={"i":i, "label":label}, code=""" label.components[i] = this.value; label.properties.components.change.emit(); label.change.emit(); """)]) + has_tex = True components[i] = paramDict[components[i]]["value"] components[i] = str(components[i]) if components[i] is not None else '' components[i] = meta.get(f"{components[i]}.AxisTitle", components[i]) + #if has_tex: + # components.append(" $$1$$ ") label.components = components return label @@ -2237,21 +2266,30 @@ def modify_2d_transform(transform_orig_parsed, transform_orig_js, varY, data_sou transform_orig_js.js_on_change("change", CustomJS(args={"mapper_new": transform_new}, code="mapper_new.args.current = this.args.current; mapper_new.change.emit()")) return transform_new -def makeCdsSel(cdsDict, paramDict, key): +def makeCdsSel(cdsDict, paramDict, key, filter=None): cds_used = cdsDict[key] if "cdsSel" in cds_used: - return cds_used["cdsSel"] + if filter in cds_used["cdsSel"]: + return cds_used["cdsSel"][filter] + else: + cds_used["cdsSel"] = {} + cds_used["filters"] = {} cds_name = key if key is not None else "default cds" + if filter is not None: + cds_name = cds_name + "#" + filter nPoints = cds_used.get("nPointRender",-1) if nPoints in paramDict: nPoints = paramDict[cds_used["nPointRender"]]["value"] - cdsSel = DownsamplerCDS(source=getOrMakeCdsFull(cdsDict, paramDict, key), nPoints=nPoints, name=cds_name) + cdsOrig = getOrMakeCdsFull(cdsDict, paramDict, key) + cdsSel = DownsamplerCDS(source=cdsOrig, nPoints=nPoints, name=cds_name) if cds_used["nPointRender"] in paramDict: paramDict[cds_used["nPointRender"]]["subscribed_events"].append(["value", CustomJS(args={"downsampler": cdsSel}, code=""" downsampler.nPoints = this.value | 0 downsampler.update() """)]) - cds_used["cdsSel"] = cdsSel + cds_used["cdsSel"][filter] = cdsSel + if filter is not None: + cds_used["filters"][filter] = ColumnFilter(source=cdsOrig, field=filter) return cdsSel def makeDescriptionTable(cdsDict, cdsName, fields, meta_fields, **kwargs): diff --git a/RootInteractive/InteractiveDrawing/bokeh/compileVarName.py b/RootInteractive/InteractiveDrawing/bokeh/compileVarName.py index 714a744a..5d54717d 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/compileVarName.py +++ b/RootInteractive/InteractiveDrawing/bokeh/compileVarName.py @@ -73,13 +73,14 @@ def __init__(self, context, cdsDict, paramDict, funcDict, code, aliasDict, first self.context = context self.dependencies = set() self.paramDependencies = set() - self.aliasDependencies = set() + self.aliasDependencies = {} self.firstGeneratedID = firstGeneratedID self.code = code self.isSource = True self.aliasDict = aliasDict self.isAuto = False self.locals = [] + self.helper_idx = 0 def visit(self, node): if isinstance(node, ast.Attribute): @@ -88,7 +89,7 @@ def visit(self, node): return self.visit_Call(node) elif isinstance(node, ast.Name): return self.visit_Name(node) - elif isinstance(node, ast.Num): + elif isinstance(node, ast.Num) or isinstance(node, ast.Constant): return self.visit_Num(node) elif isinstance(node, ast.BinOp): return self.visit_BinOp(node) @@ -119,10 +120,19 @@ def visit_Call(self, node: ast.Call): return { "implementation": implementation, "type": "javascript", - "name": self.code + "name": self.code, + "is_customjs_func": left.get("is_customjs", False) } - def visit_Num(self, node: ast.Num): + def visit_Num(self, node: ast.Constant): + if isinstance(node.value, bool): + return { + "name": str(node.n), + "implementation": "true" if node.n else "false", + "type": "constant", + "value": node.n, + "is_boolean": True + } # Kept for compatibility with old Python return { "name": str(node.n), @@ -145,7 +155,7 @@ def visit_Attribute(self, node: ast.Attribute): elif self.aliasDict[self.context][node.attr].get("fields", None) is not None: for i in self.aliasDict[self.context][node.attr]["fields"]: self.dependencies.add((self.context, i)) - self.aliasDependencies.add(node.attr) + self.aliasDependencies[node.attr] = node.attr return { "name": node.attr, "implementation": node.attr, @@ -176,10 +186,16 @@ def visit_Attribute(self, node: ast.Attribute): if(cds_used < len(attrChain)): if attrChain[cds_used] in [cds["left"], cds["right"]]: self.dependencies.add((attrChain[cds_used], node.attr)) - self.aliasDependencies.add(node.attr) + if attrChain[0] == "self": + attrChainStr = '.'.join(attrChain[1:] + [node.attr]) + else: + attrChainStr = '.'.join(attrChain + [node.attr]) + if attrChainStr not in self.aliasDependencies: + self.aliasDependencies[attrChainStr] = "$parameter_" + str(self.helper_idx) + self.helper_idx += 1 return { "name": node.attr, - "implementation": node.attr, + "implementation": self.aliasDependencies[attrChainStr], "type": "column" } if not isinstance(node.value, ast.Name): @@ -208,7 +224,7 @@ def visit_Attribute(self, node: ast.Attribute): # "error": KeyError, # "msg": "Column " + id + " not found in data source " + self.cdsDict[self.context]["name"] #} - self.aliasDependencies.add(node.attr) + self.aliasDependencies[node.attr] = node.attr try: is_boolean = "data" in self.cdsDict[self.context] and self.cdsDict[self.context]["data"][node.attr].dtype.kind == "b" except AttributeError: @@ -248,8 +264,9 @@ def visit_Name(self, node: ast.Name): return { "name": node.id, "implementation": node.id, - "n_args": len(self.funcDict[node.id]["parameters"]), - "type": "js_lambda" + "n_args": len(self.funcDict[node.id].parameters), + "type": "js_lambda", + "is_customjs": True } if node.id in self.paramDict: self.isSource = False @@ -308,7 +325,7 @@ def visit_Name_histogram(self, id: str): # "error": KeyError, # "msg": "Column " + id + " not found in histogram " + histogram["name"] #} - self.aliasDependencies.add(id) + self.aliasDependencies[id] = id return { "name": id, "implementation": id, @@ -516,8 +533,8 @@ def getOrMakeColumns(variableNames, context = None, cdsDict: dict = {}, paramDic aliasDict[i_context] = {} columnName = column["name"] func = "return "+column["implementation"] - variablesAlias = list(evaluator.aliasDependencies) - fieldsAlias = list(evaluator.aliasDependencies) + variablesAlias = list(evaluator.aliasDependencies.keys()) + fieldsAlias = list(evaluator.aliasDependencies.values()) parameters = {i:paramDict[i]["value"] for i in evaluator.paramDependencies if "options" not in paramDict[i]} variablesParam = [i for i in evaluator.paramDependencies if "options" in paramDict[i]] nvars_local = len(variablesAlias) @@ -531,7 +548,11 @@ def getOrMakeColumns(variableNames, context = None, cdsDict: dict = {}, paramDic variablesAlias.append(paramDict[j]["value"]) fieldsAlias.append(j) nvars_local = nvars_local+1 - transform = CustomJSNAryFunction(parameters=parameters, fields=fieldsAlias, func=func) + is_customjs_func = column.get("is_customjs_func", False) + if is_customjs_func: + transform = funcDict[queryAST.body.func.id] + else: + transform = CustomJSNAryFunction(parameters=parameters, fields=fieldsAlias, func=func) for j in parameters: if "subscribed_events" not in paramDict[j]: paramDict[j]["subscribed_events"] = [] diff --git a/RootInteractive/InteractiveDrawing/bokeh/test_bokehClientHistogram.py b/RootInteractive/InteractiveDrawing/bokeh/test_bokehClientHistogram.py index a1a0e747..f99aaabd 100644 --- a/RootInteractive/InteractiveDrawing/bokeh/test_bokehClientHistogram.py +++ b/RootInteractive/InteractiveDrawing/bokeh/test_bokehClientHistogram.py @@ -1,6 +1,6 @@ from RootInteractive.InteractiveDrawing.bokeh.bokehDrawSA import * from RootInteractive.InteractiveDrawing.bokeh.bokehTools import mergeFigureArrays -from RootInteractive.InteractiveDrawing.bokeh.bokehInteractiveTemplate import getDefaultVarsDiff, getDefaultVarsRatio, getDefaultVarsNormAll +from RootInteractive.InteractiveDrawing.bokeh.bokehInteractiveTemplate import getDefaultVarsDiff, getDefaultVarsRatio, getDefaultVarsNormAll, getDefaultVarsRefWeights from RootInteractive.InteractiveDrawing.bokeh.bokehInteractiveParameters import figureParameters from RootInteractive.Tools.compressArray import arrayCompressionRelative16 from pandas import CategoricalDtype @@ -456,6 +456,21 @@ def test_interactiveTemplateMultiDiff(): bokehDrawSA.fromArray(df, None, figureArray, widgetParams, layout=figureLayoutDesc, parameterArray=parameterArray, widgetLayout=widgetLayoutDesc, sizing_mode="scale_width", histogramArray=histoArray, aliasArray=aliasArray, arrayCompression=arrayCompressionRelative16, jsFunctionArray=jsFunctionArray) + +def test_interactiveTemplateWeightsRef(): + output_file("test_histogramTemplateWeightsRef.html") + aliasArray, jsFunctionArray, variables, parameterArray, widgetParams, widgetLayoutDesc, histoArray, figureArray, figureLayoutDesc = getDefaultVarsRefWeights(variables=["A", "B", "C", "D", "A*A", "A*A+B", "B/(1+C)"]) + widgetsSelect = [ + ['range', ['A'], {"name":"A"}], + ['range', ['B'], {"name":"B"}], + ['range', ['C'], {"name":"C"}], + ['range', ['D'], {"name":"D"}], + ] + widgetParams = mergeFigureArrays(widgetParams, widgetsSelect) + widgetLayoutDesc["Select"] = [["A","B"],["C","D"]] + bokehDrawSA.fromArray(df, None, figureArray, widgetParams, layout=figureLayoutDesc, parameterArray=parameterArray, + widgetLayout=widgetLayoutDesc, sizing_mode="scale_width", histogramArray=histoArray, aliasArray=aliasArray, arrayCompression=arrayCompressionRelative16, + jsFunctionArray=jsFunctionArray) def test_interactiveTemplateMultiYDiff(): output_file("test_histogramTemplateMultiYDiff.html") @@ -471,5 +486,4 @@ def test_interactiveTemplateMultiYDiff(): bokehDrawSA.fromArray(df, None, figureArray, widgetParams, layout=figureLayoutDesc, parameterArray=parameterArray, widgetLayout=widgetLayoutDesc, sizing_mode="scale_width", histogramArray=histoArray, aliasArray=aliasArray, arrayCompression=arrayCompressionRelative16, jsFunctionArray=jsFunctionArray) - -test_interactiveTemplateMultiY() \ No newline at end of file + \ No newline at end of file