@sisense/sdk-data 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -721,6 +721,146 @@ export declare const topRanking: (attribute: Attribute, measure: Measure, count:
721
721
  * @returns A filter instance
722
722
  */
723
723
  export declare const bottomRanking: (attribute: Attribute, measure: Measure, count: number, config?: BaseFilterConfig) => Filter;
724
+ /**
725
+ * Creates a filter that returns the top N values of the last dimension,
726
+ * independently for each unique combination of all preceding dimensions.
727
+ *
728
+ * This filter applies ranking within groups rather than globally. It shows the top N values
729
+ * of the rightmost dimension for every unique combination of the other dimensions to its left.
730
+ * The order of dimensions in your query determines the grouping behavior.
731
+ *
732
+ * **Key Differences from {@link topRanking}:**
733
+ * - `topRanking`: Filters a specific dimension globally (you specify which dimension)
734
+ * - `measureTopRanking`: Always filters the last/rightmost dimension, grouped by all others
735
+ *
736
+ * **How it works:**
737
+ * - With 1 dimension: Returns the top N values of that dimension
738
+ * - With 2+ dimensions: Returns the top N values of the LAST dimension for each combination of the others
739
+ *
740
+ * @example
741
+ * **Example 1: Single dimension (equivalent to topRanking) - Query with one dimension [Category]**
742
+ * ```ts
743
+ * // Returns top 5 Categories by total revenue
744
+ * filterFactory.measureTopRanking(
745
+ * measureFactory.sum(DM.Commerce.Revenue),
746
+ * 5
747
+ * )
748
+ * ```
749
+ * Result: 5 categories (e.g., Cell Phones, Computers, TVs, etc.)
750
+ *
751
+ * This produces the same result as:
752
+ * ```ts
753
+ * filterFactory.topRanking(
754
+ * DM.Commerce.Category,
755
+ * measureFactory.sum(DM.Commerce.Revenue),
756
+ * 5
757
+ * )
758
+ * ```
759
+ *
760
+ * **Note:** With only one dimension, there are no groups to rank within,
761
+ * so the behavior is identical to `topRanking`.
762
+ *
763
+ * @example
764
+ * **Example 2: Two dimensions - Query with dimensions [Gender, Category]**
765
+ * ```ts
766
+ * // Returns top 2 Categories for each Gender
767
+ * filterFactory.measureTopRanking(
768
+ * measureFactory.sum(DM.Commerce.Revenue),
769
+ * 2
770
+ * )
771
+ * ```
772
+ * Result: 3 genders × 2 categories each = 6 rows
773
+ * - Male: Top 2 categories by revenue
774
+ * - Female: Top 2 categories by revenue
775
+ * - Unspecified: Top 2 categories by revenue
776
+ *
777
+ * @example
778
+ * **Example 3: Three dimensions - Query with dimensions [Gender, Age Range, Category]**
779
+ * ```ts
780
+ * // Returns top 2 Categories for each (Gender, Age Range) combination
781
+ * filterFactory.measureTopRanking(
782
+ * measureFactory.sum(DM.Commerce.Revenue),
783
+ * 2
784
+ * )
785
+ * ```
786
+ * Result: 3 genders × 7 age ranges × 2 categories per combination = ~42 rows
787
+ *
788
+ * @param measure - Base measure to rank by
789
+ * @param count - Number of items to return per group (applies to the last dimension)
790
+ * @param config - Optional configuration for the filter
791
+ * @returns A filter instance
792
+ */
793
+ export declare const measureTopRanking: (measure: BaseMeasure, count: number, config?: BaseFilterConfig) => Filter;
794
+ /**
795
+ * Creates a filter that returns the bottom N values of the last dimension,
796
+ * independently for each unique combination of all preceding dimensions.
797
+ *
798
+ * This filter applies ranking within groups rather than globally. It shows the bottom N values
799
+ * of the rightmost dimension for every unique combination of the other dimensions to its left.
800
+ * The order of dimensions in your query determines the grouping behavior.
801
+ *
802
+ * **Key Differences from {@link bottomRanking}:**
803
+ * - `bottomRanking`: Filters a specific dimension globally (you specify which dimension)
804
+ * - `measureBottomRanking`: Always filters the last/rightmost dimension, grouped by all others
805
+ *
806
+ * **How it works:**
807
+ * - With 1 dimension: Returns the bottom N values of that dimension
808
+ * - With 2+ dimensions: Returns the bottom N values of the LAST dimension for each combination of the others
809
+ *
810
+ * @example
811
+ * **Example 1: Single dimension (equivalent to bottomRanking) - Query with one dimension [Category]**
812
+ * ```ts
813
+ * // Returns bottom 5 Categories by revenue
814
+ * filterFactory.measureBottomRanking(
815
+ * measureFactory.sum(DM.Commerce.Revenue),
816
+ * 5
817
+ * )
818
+ * ```
819
+ * Result: 5 categories with lowest revenue (e.g., Accessories, Cables, etc.)
820
+ *
821
+ * This produces the same result as:
822
+ * ```ts
823
+ * filterFactory.bottomRanking(
824
+ * DM.Commerce.Category,
825
+ * measureFactory.sum(DM.Commerce.Revenue),
826
+ * 5
827
+ * )
828
+ * ```
829
+ *
830
+ * **Note:** With only one dimension, there are no groups to rank within,
831
+ * so the behavior is identical to `bottomRanking`.
832
+ *
833
+ * @example
834
+ * **Example 2: Two dimensions - Query with dimensions [Gender, Category]**
835
+ * ```ts
836
+ * // Returns bottom 2 Categories for each Gender
837
+ * filterFactory.measureBottomRanking(
838
+ * measureFactory.sum(DM.Commerce.Revenue),
839
+ * 2
840
+ * )
841
+ * ```
842
+ * Result: 3 genders × 2 categories each = 6 rows
843
+ * - Male: Bottom 2 categories by revenue
844
+ * - Female: Bottom 2 categories by revenue
845
+ * - Unspecified: Bottom 2 categories by revenue
846
+ *
847
+ * @example
848
+ * **Example 3: Three dimensions - Query with dimensions [Gender, Age Range, Category]**
849
+ * ```ts
850
+ * // Returns bottom 2 Categories for each (Gender, Age Range) combination
851
+ * filterFactory.measureBottomRanking(
852
+ * measureFactory.sum(DM.Commerce.Revenue),
853
+ * 2
854
+ * )
855
+ * ```
856
+ * Result: 3 genders × 7 age ranges × 2 categories per combination = ~42 rows
857
+ *
858
+ * @param measure - Base measure to rank by
859
+ * @param count - Number of items to return per group (applies to the last dimension)
860
+ * @param config - Optional configuration for the filter
861
+ * @returns A filter instance
862
+ */
863
+ export declare const measureBottomRanking: (measure: BaseMeasure, count: number, config?: BaseFilterConfig) => Filter;
724
864
  /**
725
865
  * Creates a filter that contains a list of dependent/cascading filters,
726
866
  * where each filter depends on the results or state of the previous ones in the array.
@@ -2,7 +2,7 @@
2
2
  /* eslint-disable max-params */
3
3
  /* eslint-disable @typescript-eslint/no-shadow */
4
4
  import { withComposeCodeForFilter, withComposeCodeForFilterRelations, } from '../compose-code-utils.js';
5
- import { CascadingFilter, CustomFilter, DateOperators, DateRangeFilter, ExcludeFilter, LogicalAttributeFilter, LogicalOperators, MeasureFilter, MembersFilter, NumericFilter, NumericOperators, RankingFilter, RankingOperators, RelativeDateFilter, TextFilter, TextOperators, } from './filters.js';
5
+ import { CascadingFilter, CustomFilter, DateOperators, DateRangeFilter, ExcludeFilter, LogicalAttributeFilter, LogicalOperators, MeasureFilter, MeasureRankingFilter, MembersFilter, NumericFilter, NumericOperators, RankingFilter, RankingOperators, RelativeDateFilter, TextFilter, TextOperators, } from './filters.js';
6
6
  // LOGICAL FILTERS
7
7
  /**
8
8
  * Creates a filter representing the union of multiple filters on the same attribute. The resulting
@@ -746,19 +746,146 @@ export const topRanking = withComposeCodeForFilter((attribute, measure, count, c
746
746
  * @returns A filter instance
747
747
  */
748
748
  export const bottomRanking = withComposeCodeForFilter((attribute, measure, count, config) => new RankingFilter(attribute, measure, RankingOperators.Bottom, count, config), 'bottomRanking');
749
- const relate = (node) => {
750
- if (Array.isArray(node)) {
751
- const [first, ...rest] = node;
752
- return rest.length === 0
753
- ? relate(first)
754
- : {
755
- operator: 'AND',
756
- left: relate(first),
757
- right: relate(rest),
758
- };
759
- }
760
- return node;
761
- };
749
+ /**
750
+ * Creates a filter that returns the top N values of the last dimension,
751
+ * independently for each unique combination of all preceding dimensions.
752
+ *
753
+ * This filter applies ranking within groups rather than globally. It shows the top N values
754
+ * of the rightmost dimension for every unique combination of the other dimensions to its left.
755
+ * The order of dimensions in your query determines the grouping behavior.
756
+ *
757
+ * **Key Differences from {@link topRanking}:**
758
+ * - `topRanking`: Filters a specific dimension globally (you specify which dimension)
759
+ * - `measureTopRanking`: Always filters the last/rightmost dimension, grouped by all others
760
+ *
761
+ * **How it works:**
762
+ * - With 1 dimension: Returns the top N values of that dimension
763
+ * - With 2+ dimensions: Returns the top N values of the LAST dimension for each combination of the others
764
+ *
765
+ * @example
766
+ * **Example 1: Single dimension (equivalent to topRanking) - Query with one dimension [Category]**
767
+ * ```ts
768
+ * // Returns top 5 Categories by total revenue
769
+ * filterFactory.measureTopRanking(
770
+ * measureFactory.sum(DM.Commerce.Revenue),
771
+ * 5
772
+ * )
773
+ * ```
774
+ * Result: 5 categories (e.g., Cell Phones, Computers, TVs, etc.)
775
+ *
776
+ * This produces the same result as:
777
+ * ```ts
778
+ * filterFactory.topRanking(
779
+ * DM.Commerce.Category,
780
+ * measureFactory.sum(DM.Commerce.Revenue),
781
+ * 5
782
+ * )
783
+ * ```
784
+ *
785
+ * **Note:** With only one dimension, there are no groups to rank within,
786
+ * so the behavior is identical to `topRanking`.
787
+ *
788
+ * @example
789
+ * **Example 2: Two dimensions - Query with dimensions [Gender, Category]**
790
+ * ```ts
791
+ * // Returns top 2 Categories for each Gender
792
+ * filterFactory.measureTopRanking(
793
+ * measureFactory.sum(DM.Commerce.Revenue),
794
+ * 2
795
+ * )
796
+ * ```
797
+ * Result: 3 genders × 2 categories each = 6 rows
798
+ * - Male: Top 2 categories by revenue
799
+ * - Female: Top 2 categories by revenue
800
+ * - Unspecified: Top 2 categories by revenue
801
+ *
802
+ * @example
803
+ * **Example 3: Three dimensions - Query with dimensions [Gender, Age Range, Category]**
804
+ * ```ts
805
+ * // Returns top 2 Categories for each (Gender, Age Range) combination
806
+ * filterFactory.measureTopRanking(
807
+ * measureFactory.sum(DM.Commerce.Revenue),
808
+ * 2
809
+ * )
810
+ * ```
811
+ * Result: 3 genders × 7 age ranges × 2 categories per combination = ~42 rows
812
+ *
813
+ * @param measure - Base measure to rank by
814
+ * @param count - Number of items to return per group (applies to the last dimension)
815
+ * @param config - Optional configuration for the filter
816
+ * @returns A filter instance
817
+ */
818
+ export const measureTopRanking = withComposeCodeForFilter((measure, count, config) => new MeasureRankingFilter(measure, RankingOperators.Top, count, config), 'measureTopRanking');
819
+ /**
820
+ * Creates a filter that returns the bottom N values of the last dimension,
821
+ * independently for each unique combination of all preceding dimensions.
822
+ *
823
+ * This filter applies ranking within groups rather than globally. It shows the bottom N values
824
+ * of the rightmost dimension for every unique combination of the other dimensions to its left.
825
+ * The order of dimensions in your query determines the grouping behavior.
826
+ *
827
+ * **Key Differences from {@link bottomRanking}:**
828
+ * - `bottomRanking`: Filters a specific dimension globally (you specify which dimension)
829
+ * - `measureBottomRanking`: Always filters the last/rightmost dimension, grouped by all others
830
+ *
831
+ * **How it works:**
832
+ * - With 1 dimension: Returns the bottom N values of that dimension
833
+ * - With 2+ dimensions: Returns the bottom N values of the LAST dimension for each combination of the others
834
+ *
835
+ * @example
836
+ * **Example 1: Single dimension (equivalent to bottomRanking) - Query with one dimension [Category]**
837
+ * ```ts
838
+ * // Returns bottom 5 Categories by revenue
839
+ * filterFactory.measureBottomRanking(
840
+ * measureFactory.sum(DM.Commerce.Revenue),
841
+ * 5
842
+ * )
843
+ * ```
844
+ * Result: 5 categories with lowest revenue (e.g., Accessories, Cables, etc.)
845
+ *
846
+ * This produces the same result as:
847
+ * ```ts
848
+ * filterFactory.bottomRanking(
849
+ * DM.Commerce.Category,
850
+ * measureFactory.sum(DM.Commerce.Revenue),
851
+ * 5
852
+ * )
853
+ * ```
854
+ *
855
+ * **Note:** With only one dimension, there are no groups to rank within,
856
+ * so the behavior is identical to `bottomRanking`.
857
+ *
858
+ * @example
859
+ * **Example 2: Two dimensions - Query with dimensions [Gender, Category]**
860
+ * ```ts
861
+ * // Returns bottom 2 Categories for each Gender
862
+ * filterFactory.measureBottomRanking(
863
+ * measureFactory.sum(DM.Commerce.Revenue),
864
+ * 2
865
+ * )
866
+ * ```
867
+ * Result: 3 genders × 2 categories each = 6 rows
868
+ * - Male: Bottom 2 categories by revenue
869
+ * - Female: Bottom 2 categories by revenue
870
+ * - Unspecified: Bottom 2 categories by revenue
871
+ *
872
+ * @example
873
+ * **Example 3: Three dimensions - Query with dimensions [Gender, Age Range, Category]**
874
+ * ```ts
875
+ * // Returns bottom 2 Categories for each (Gender, Age Range) combination
876
+ * filterFactory.measureBottomRanking(
877
+ * measureFactory.sum(DM.Commerce.Revenue),
878
+ * 2
879
+ * )
880
+ * ```
881
+ * Result: 3 genders × 7 age ranges × 2 categories per combination = ~42 rows
882
+ *
883
+ * @param measure - Base measure to rank by
884
+ * @param count - Number of items to return per group (applies to the last dimension)
885
+ * @param config - Optional configuration for the filter
886
+ * @returns A filter instance
887
+ */
888
+ export const measureBottomRanking = withComposeCodeForFilter((measure, count, config) => new MeasureRankingFilter(measure, RankingOperators.Bottom, count, config), 'measureBottomRanking');
762
889
  // CASCADING FILTERS
763
890
  /**
764
891
  * Creates a filter that contains a list of dependent/cascading filters,
@@ -815,6 +942,26 @@ export const cascading = withComposeCodeForFilter((filters, config) => new Casca
815
942
  // eslint-disable-next-line @typescript-eslint/no-namespace
816
943
  export var logic;
817
944
  (function (logic) {
945
+ /**
946
+ * Relates a filter or filter relations node
947
+ *
948
+ * @param node - Filter or filter relations node
949
+ * @returns Related filter or filter relations node
950
+ * @internal
951
+ */
952
+ const relate = (node) => {
953
+ if (Array.isArray(node)) {
954
+ const [first, ...rest] = node;
955
+ return rest.length === 0
956
+ ? relate(first)
957
+ : {
958
+ operator: 'AND',
959
+ left: relate(first),
960
+ right: relate(rest),
961
+ };
962
+ }
963
+ return node;
964
+ };
818
965
  /**
819
966
  * Creates an 'AND' filter relations
820
967
  *
@@ -1,5 +1,5 @@
1
1
  import { DimensionalElement } from '../base.js';
2
- import { Attribute, BaseFilterConfig, CompleteBaseFilterConfig, CompleteMembersFilterConfig, Filter, LevelAttribute, Measure, MembersFilterConfig } from '../interfaces.js';
2
+ import { Attribute, BaseFilterConfig, BaseMeasure, CompleteBaseFilterConfig, CompleteMembersFilterConfig, Filter, LevelAttribute, Measure, MembersFilterConfig } from '../interfaces.js';
3
3
  import { AnyObject, JSONObject } from '../types.js';
4
4
  /**
5
5
  * Different text operators that can be used with text filters
@@ -70,6 +70,7 @@ export declare const FilterTypes: {
70
70
  readonly exclude: "exclude";
71
71
  readonly measure: "measure";
72
72
  readonly ranking: "ranking";
73
+ readonly measureRanking: "measure-ranking";
73
74
  readonly text: "text";
74
75
  readonly numeric: "numeric";
75
76
  readonly dateRange: "dateRange";
@@ -298,6 +299,32 @@ export declare class RankingFilter extends AbstractFilter {
298
299
  */
299
300
  filterJaql(): any;
300
301
  }
302
+ /**
303
+ * @internal
304
+ */
305
+ export declare class MeasureRankingFilter extends AbstractFilter {
306
+ /**
307
+ * @internal
308
+ */
309
+ readonly __serializable: string;
310
+ count: number;
311
+ operator: string;
312
+ measure: BaseMeasure;
313
+ constructor(measure: BaseMeasure, operator: string, count: number, config?: BaseFilterConfig, composeCode?: string);
314
+ /**
315
+ * gets the element's ID
316
+ */
317
+ get id(): string;
318
+ /**
319
+ * Gets a serializable representation of the element
320
+ */
321
+ serialize(): JSONObject;
322
+ /**
323
+ * Gets JAQL representing this Filter instance
324
+ */
325
+ filterJaql(): any;
326
+ jaql(nested?: boolean | undefined): any;
327
+ }
301
328
  /**
302
329
  * @internal
303
330
  */
@@ -427,6 +454,13 @@ export declare function isNumericFilter(filter: Filter & AnyObject): filter is N
427
454
  * @internal
428
455
  */
429
456
  export declare function isRankingFilter(filter: Filter & AnyObject): filter is RankingFilter;
457
+ /**
458
+ * Checks if a filter is a MeasureRankingFilter.
459
+ *
460
+ * @param filter - The filter to check.
461
+ * @internal
462
+ */
463
+ export declare function isMeasureRankingFilter(filter: Filter & AnyObject): filter is MeasureRankingFilter;
430
464
  /**
431
465
  * Checks if a filter is a MeasureFilter.
432
466
  *
@@ -77,6 +77,7 @@ export const FilterTypes = {
77
77
  exclude: 'exclude',
78
78
  measure: 'measure',
79
79
  ranking: 'ranking',
80
+ measureRanking: 'measure-ranking',
80
81
  text: 'text',
81
82
  numeric: 'numeric',
82
83
  dateRange: 'dateRange',
@@ -517,6 +518,59 @@ export class RankingFilter extends AbstractFilter {
517
518
  return result;
518
519
  }
519
520
  }
521
+ /**
522
+ * @internal
523
+ */
524
+ export class MeasureRankingFilter extends AbstractFilter {
525
+ constructor(measure, operator, count, config, composeCode) {
526
+ super(measure.attribute, FilterTypes.measureRanking, config, composeCode);
527
+ /**
528
+ * @internal
529
+ */
530
+ this.__serializable = 'MeasureRankingFilter';
531
+ this.count = count;
532
+ this.operator = operator;
533
+ this.measure = measure;
534
+ }
535
+ /**
536
+ * gets the element's ID
537
+ */
538
+ get id() {
539
+ return `${this.operator}_${this.count}_measure_${this.measure.id}`;
540
+ }
541
+ /**
542
+ * Gets a serializable representation of the element
543
+ */
544
+ serialize() {
545
+ const result = super.serialize();
546
+ result.measure = this.measure.serialize();
547
+ result.count = this.count;
548
+ result.operator = this.operator;
549
+ return result;
550
+ }
551
+ /**
552
+ * Gets JAQL representing this Filter instance
553
+ */
554
+ filterJaql() {
555
+ const result = {};
556
+ result[this.operator] = this.count;
557
+ return result;
558
+ }
559
+ jaql(nested) {
560
+ if (this.config.disabled) {
561
+ return AbstractFilter.disabledJaql(nested);
562
+ }
563
+ const result = super.jaql(nested);
564
+ if (isDimensionalBaseMeasure(this.measure)) {
565
+ Object.entries(this.measure.jaql().jaql).forEach(([key, value]) => {
566
+ result.jaql[key] = value;
567
+ });
568
+ }
569
+ // Add type: 'measure' for measure-based filters
570
+ result.jaql.type = 'measure';
571
+ return result;
572
+ }
573
+ }
520
574
  /**
521
575
  * @internal
522
576
  */
@@ -734,6 +788,15 @@ export function isNumericFilter(filter) {
734
788
  export function isRankingFilter(filter) {
735
789
  return filter && filter.__serializable === 'RankingFilter';
736
790
  }
791
+ /**
792
+ * Checks if a filter is a MeasureRankingFilter.
793
+ *
794
+ * @param filter - The filter to check.
795
+ * @internal
796
+ */
797
+ export function isMeasureRankingFilter(filter) {
798
+ return filter && filter.__serializable === 'MeasureRankingFilter';
799
+ }
737
800
  /**
738
801
  * Checks if a filter is a MeasureFilter.
739
802
  *
@@ -805,31 +868,24 @@ export function createFilter(json) {
805
868
  switch (json.filterType) {
806
869
  case FilterTypes.logicalAttribute:
807
870
  return new LogicalAttributeFilter(json.filters.map((f) => createFilter(f)), json.operator);
808
- break;
809
871
  case FilterTypes.members:
810
872
  return new MembersFilter(create(json.attribute), json.members);
811
- break;
812
873
  case FilterTypes.exclude:
813
874
  return new ExcludeFilter(createFilter(json.filter), json.input && createFilter(json.input));
814
- break;
815
875
  case FilterTypes.measure:
816
876
  return new MeasureFilter(create(json.attribute), create(json.measure), json.operatorA, json.valueA, json.operatorB, json.valueB);
817
- break;
818
877
  case FilterTypes.ranking:
819
878
  return new RankingFilter(create(json.attribute), create(json.measure), json.operator, json.count);
820
- break;
879
+ case FilterTypes.measureRanking:
880
+ return new MeasureRankingFilter(create(json.measure), json.operator, json.count);
821
881
  case FilterTypes.numeric:
822
882
  return new NumericFilter(create(json.attribute), json.operatorA, json.valueA, json.operatorB, json.valueB);
823
- break;
824
883
  case FilterTypes.text:
825
884
  return new TextFilter(create(json.attribute), json.operatorA, json.valueA);
826
- break;
827
885
  case FilterTypes.relativeDate:
828
886
  return new RelativeDateFilter(create(json.attribute), json.offset, json.count, json.operator, json.anchor);
829
- break;
830
887
  case FilterTypes.dateRange:
831
888
  return new DateRangeFilter(create(json.attribute), json.valueA, json.valueB);
832
- break;
833
889
  }
834
890
  throw new TranslatableError('errors.filter.unsupportedType', {
835
891
  filterType: json.filterType,
@@ -181,6 +181,10 @@ export const createMeasureFilterFromConditionFilterJaql = (measure, conditionFil
181
181
  return filterFactory.measureBetween(measure, conditionFilterJaql.from, conditionFilterJaql.to, { guid });
182
182
  case ConditionFilterType.IS_NOT_BETWEEN:
183
183
  return filterFactory.exclude(filterFactory.measureBetween(measure, (_a = conditionFilterJaql.exclude) === null || _a === void 0 ? void 0 : _a.from, (_b = conditionFilterJaql.exclude) === null || _b === void 0 ? void 0 : _b.to, { guid }), undefined, { guid });
184
+ case ConditionFilterType.TOP:
185
+ return filterFactory.measureTopRanking(measure, conditionFilterJaql[ConditionFilterType.TOP], { guid });
186
+ case ConditionFilterType.BOTTOM:
187
+ return filterFactory.measureBottomRanking(measure, conditionFilterJaql[ConditionFilterType.BOTTOM], { guid });
184
188
  }
185
189
  throw new TranslatableError('errors.filter.unsupportedConditionFilter', {
186
190
  filter: JSON.stringify(conditionFilterJaql),