import NumberColumn from './NumberColumn' ; import type { IEventListener } from '../internal' ; import { SortByDefault , toolbar } from './annotations' ; import Column , { dirty , dirtyCaches , dirtyHeader , dirtyValues , groupRendererChanged , labelChanged , metaDataChanged , rendererTypeChanged , summaryRendererChanged , visibilityChanged , widthChanged , } from './Column' ; import type { addColumn , moveColumn , removeColumn } from './CompositeColumn' ; import type CompositeColumn from './CompositeColumn' ; import CompositeNumberColumn , { ICompositeNumberDesc } from './CompositeNumberColumn' ; import type { IDataRow , ITypeFactory } from './interfaces' ; import { isDummyNumberFilter , noNumberFilter , restoreNumberFilter } from './internalNumber' ; import { IColorMappingFunction , IMapAbleColumn , IMapAbleDesc , IMappingFunction , INumberFilter , isNumberColumn , } from './INumberColumn' ; import { restoreMapping } from './MappingFunction' ; import { integrateDefaults } from './internal' ; const DEFAULT_SCRIPT = `let s = 0;
col.forEach((c) => s += c.v);
return s / col.length` ; param label returns { { type : string , label : string } } export function createScriptDesc ( label = 'script' ) { return { type : 'script' , label , script : DEFAULT_SCRIPT } ; } function wrapWithContext ( code ) { let clean = code . trim ( ) ; if ( ! clean . includes ( 'return' ) ) { clean = `return (${ clean });` ; } return `
  const max = function(arr) { return Math.max.apply(Math, arr); };
  const min = function(arr) { return Math.min.apply(Math, arr); };
  const extent = function(arr) { return [min(arr), max(arr)]; };
  const clamp = function(v, minValue, maxValue) { return v < minValue ? minValue : (v > maxValue ? maxValue : v); };
  const normalize = function(v, minMax, max) {
    if (Array.isArray(minMax)) {
      minMax = minMax[0];
      max = minMax[1];
    }
    return (v - minMax) / (max - minMax);
  };
  const denormalize = function(v, minMax, max) {
    if (Array.isArray(minMax)) {
      minMax = minMax[0];
      max = minMax[1];
    }
    return v * (max - minMax) + minMax;
  };
  const linear = function(v, source, target) {
    target = target || [0, 1];
    return denormalize(normalize(v, source), target);
  };
  const v = (function custom() {
    ${ clean }
  })();

  return typeof v === 'number' ? v : NaN` ; } interface IColumnWrapper { v : any ; raw : number | null ; type : string ; name : string ; id : string ; } /**
 * wrapper class for simpler column accessing
 */ class ColumnWrapper implements IColumnWrapper { constructor ( private readonly c , public readonly v , public readonly raw ) { } get type ( ) { return this . c . desc . type ; } get name ( ) { return this . c . getMetaData ( ) . label ; } get id ( ) { return this . c . id ; } } class LazyColumnWrapper implements IColumnWrapper { constructor ( private readonly c , private readonly row ) { } get type ( ) { return this . c . desc . type ; } get name ( ) { return this . c . getMetaData ( ) . label ; } get id ( ) { return this . c . id ; } get v ( ) { return this . c . getValue ( this . row ) ; } get raw ( ) { return isNumberColumn ( this . c ) ? this . c . getRawNumber ( this . row ) : null ; } } /**
 * helper context for accessing columns within a scripted columns
 */ class ColumnContext { private readonly lookup = new Map < string , IColumnWrapper > ( ) ; private _all : ColumnContext | null = null ; constructor ( private readonly children , private readonly allFactory ? ) { children . forEach ( ( c ) => { this . lookup . set ( `ID@${ c . id }` , c ) ; this . lookup . set ( `ID@${ c . id . toLowerCase ( ) }` , c ) ; this . lookup . set ( `NAME@${ c . name }` , c ) ; this . lookup . set ( `NAME@${ c . name . toLowerCase ( ) }` , c ) ; } ) ; } param { string } name return { IColumnWrapper } byName ( name ) { return this . lookup . get ( `NAME@${ name }` ) ; } param { string } id return { IColumnWrapper } byID ( id ) { return this . lookup . get ( `ID@${ id }` ) ; } param { number } index return { IColumnWrapper } byIndex ( index ) { return this . children [ index ] ; } forEach ( callback ) { return this . children . forEach ( callback ) ; } return { number } get length ( ) { return this . children . length ; } return { ColumnContext } get all ( ) { if ( this . _all == null ) { this . _all = this . allFactory ? this . allFactory ( ) : null ; } return this . _all ! ; } } export interface IScriptDesc extends ICompositeNumberDesc , IMapAbleDesc { default script ? : string ; } export declare type IScriptColumnDesc = IScriptDesc ; asMemberOf event export declare function scriptChanged ( previous , current ) ; asMemberOf event export declare function mappingChanged ( previous , current ) ; asMemberOf event export declare function colorMappingChanged ( previous , current ) ; asMemberOf event export declare function filterChanged ( previous , current ) ; /**
 * column combiner which uses a custom JavaScript function to combined the values
 * The script itself can be any valid JavaScript code. It will be embedded in a function.
 * Therefore the last statement has to return a value.
 *
 * In case of a single line statement the code piece statement <code>return</code> will be automatically prefixed.
 *
 * The function signature is: <br><code>(row: any, index: number, children: Column[], values: any[], raws: (number|null)[]) => number</code>
 *  <dl>
 *    <dt>param: <code>row</code></dt>
 *    <dd>the row in the dataset to compute the value for</dd>
 *    <dt>param: <code>index</code></dt>
 *    <dd>the index of the row</dd>
 *    <dt>param: <code>children</code></dt>
 *    <dd>the list of LineUp columns that are part of this ScriptColumn</dd>
 *    <dt>param: <code>values</code></dt>
 *    <dd>the computed value of each column (see <code>children</code>) for the current row</dd>
 *    <dt>param: <code>raws</code></dt>
 *    <dd>similar to <code>values</code>. Numeric columns return by default the normalized value, this array gives access to the original "raw" values before mapping is applied</dd>
 *    <dt>returns:</dt>
 *    <dd>the computed number <strong>in the range [0, 1] or NaN</strong></dd>
 *  </dl>
 *
 * In addition to the standard JavaScript functions and objects (Math, ...) a couple of utility functions are available: </p>
 * <dl>
 *    <dt><code>max(arr: number[]) => number</code></dt>
 *    <dd>computes the maximum of the given array of numbers</dd>
 *    <dt><code>min(arr: number[]) => number</code></dt>
 *    <dd>computes the minimum of the given array of numbers</dd>
 *    <dt><code>extent(arr: number[]) => [number, number]</code></dt>
 *    <dd>computes both minimum and maximum and returning an array with the first element the minimum and the second the maximum</dd>
 *    <dt><code>clamp(v: number, min: number, max: number) => number</code></dt>
 *    <dd>ensures that the given value is within the given min/max value</dd>
 *    <dt><code>normalize(v: number, min: number, max: number) => number</code></dt>
 *    <dd>normalizes the given value <code>(v - min) / (max - min)</code></dd>
 *    <dt><code>denormalize(v: number, min: number, max: number) => number</code></dt>
 *    <dd>inverts a normalized value <code>v * (max - min) + min</code></dd>
 *    <dt><code>linear(v: number, input: [number, number], output: [number, number]) => number</code></dt>
 *    <dd>performs a linear mapping from input domain to output domain both given as an array of [min, max] values. <code>denormalize(normalize(v, input[0], input[1]), output[0], output[1])</code></dd>
 *  </dl>
 */ @ toolbar ( 'script' , 'filterNumber' , 'colorMapped' , 'editMapping' ) @ SortByDefault ( 'descending' ) export default class ScriptColumn extends CompositeNumberColumn implements IMapAbleColumn { static readonly EVENT_MAPPING_CHANGED = NumberColumn . EVENT_MAPPING_CHANGED ; static readonly EVENT_COLOR_MAPPING_CHANGED = NumberColumn . EVENT_COLOR_MAPPING_CHANGED ; static readonly EVENT_SCRIPT_CHANGED = 'scriptChanged' ; static readonly DEFAULT_SCRIPT = DEFAULT_SCRIPT ; private script = ScriptColumn . DEFAULT_SCRIPT ; private f : ( children , values : number [ ] , raws : any [ ] , col : ColumnContext , row : any , index : number ) => number | null = null ; private mapping : IMappingFunction ; private original : IMappingFunction ; private colorMapping : IColorMappingFunction ; type { { min : number , max : number } } private private currentFilter : INumberFilter = noNumberFilter ( ) ; constructor ( id , desc , factory ) { super ( id , integrateDefaults ( desc , { renderer : 'number' , groupRenderer : 'boxplot' , summaryRenderer : 'histogram' , } ) ) ; this . script = desc . script || this . script ; this . mapping = restoreMapping ( desc , factory ) ; this . original = this . mapping . clone ( ) ; this . colorMapping = factory . colorMappingFunction ( desc . colorMapping || desc . color ) ; } protected createEventList ( ) { return super . createEventList ( ) . concat ( [ ScriptColumn . EVENT_SCRIPT_CHANGED , ScriptColumn . EVENT_COLOR_MAPPING_CHANGED , ScriptColumn . EVENT_MAPPING_CHANGED , ] ) ; } on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) ; on ( type , listener ) { return super . on ( type , listener ) ; } setScript ( script ) { if ( this . script === script ) { return ; } this . f = null ; this . fire ( [ ScriptColumn . EVENT_SCRIPT_CHANGED , Column . EVENT_DIRTY_VALUES , Column . EVENT_DIRTY_CACHES , Column . EVENT_DIRTY ] , this . script , ( this . script = script ) ) ; } getScript ( ) { return this . script ; } dump ( toDescRef ) { const r = super . dump ( toDescRef ) ; r . script = this . script ; r . filter = ! isDummyNumberFilter ( this . currentFilter ) ? this . currentFilter : null ; r . map = this . mapping . toJSON ( ) ; r . colorMapping = this . colorMapping . toJSON ( ) ; return r ; } restore ( dump , factory ) { super . restore ( dump , factory ) ; this . script = dump . script || this . script ; if ( dump . filter ) { this . currentFilter = restoreNumberFilter ( dump . filter ) ; } if ( dump . map || dump . domain ) { this . mapping = restoreMapping ( dump . map , factory ) ; } if ( dump . colorMapping ) { this . colorMapping = factory . colorMappingFunction ( dump . colorMapping ) ; } } protected compute ( row ) { if ( this . f == null ) { this . f = new Function ( 'children' , 'values' , 'raws' , 'col' , 'row' , 'index' , wrapWithContext ( this . script ) ) as any ; } const children = this . _children ; const values = this . _children . map ( ( d ) => d . getValue ( row ) ) ; const raws = this . _children . map ( ( d ) => ( isNumberColumn ( d ) ? d . getRawNumber ( row ) : null ) ) as number [ ] ; const col = new ColumnContext ( children . map ( ( c , i ) => new ColumnWrapper ( c , values [ i ] , raws [ i ] ) ) , ( ) => { const cols = this . findMyRanker ( ) ! . flatColumns ; return new ColumnContext ( cols . map ( ( c ) => new LazyColumnWrapper ( c , row ) ) ) ; } ) ; return this . f . call ( this , children , values , raws , col , row . v , row . i ) ; } getExportValue ( row , format ) { if ( format === 'json' ) { return { value : this . getRawNumber ( row ) , children : this . children . map ( ( d ) => d . getExportValue ( row , format ) ) , } ; } return super . getExportValue ( row , format ) ; } getRange ( ) { return this . mapping . getRange ( this . getNumberFormat ( ) ) ; } getOriginalMapping ( ) { return this . original . clone ( ) ; } getMapping ( ) { return this . mapping . clone ( ) ; } setMapping ( mapping ) { if ( this . mapping . eq ( mapping ) ) { return ; } this . fire ( [ ScriptColumn . EVENT_MAPPING_CHANGED , Column . EVENT_DIRTY_HEADER , Column . EVENT_DIRTY_VALUES , Column . EVENT_DIRTY_CACHES , Column . EVENT_DIRTY , ] , this . mapping . clone ( ) , ( this . mapping = mapping ) ) ; } getColor ( row ) { return NumberColumn . prototype . getColor . call ( this , row ) ; } getColorMapping ( ) { return this . colorMapping . clone ( ) ; } setColorMapping ( mapping ) { if ( this . colorMapping . eq ( mapping ) ) { return ; } this . fire ( [ ScriptColumn . EVENT_COLOR_MAPPING_CHANGED , Column . EVENT_DIRTY_HEADER , Column . EVENT_DIRTY_VALUES , Column . EVENT_DIRTY_CACHES , Column . EVENT_DIRTY , ] , this . colorMapping . clone ( ) , ( this . colorMapping = mapping ) ) ; } isFiltered ( ) { return NumberColumn . prototype . isFiltered . call ( this ) ; } getFilter ( ) { return NumberColumn . prototype . getFilter . call ( this ) ; } setFilter ( value ) { NumberColumn . prototype . setFilter . call ( this , value ) ; } filter ( row ) { return NumberColumn . prototype . filter . call ( this , row ) ; } clearFilter ( ) { return NumberColumn . prototype . clearFilter . call ( this ) ; } }