Skip to content

Commit

Permalink
refactor: replace dataObj with $rootScope.globalFilterData
Browse files Browse the repository at this point in the history
  • Loading branch information
arildm committed Jan 30, 2025
1 parent 29ddaac commit 49b2a5e
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 100 deletions.
1 change: 1 addition & 0 deletions app/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ korpApp.run([
const s = $rootScope

s.extendedCQP = null
s.globalFilterData = {}

/** This deferred is used to signal that the filter feature is ready. */
s.globalFilterDef = $q.defer<never>()
Expand Down
89 changes: 33 additions & 56 deletions app/scripts/global-filter/global-filter-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
* and then merged with the tokens of the query when sending it to the backend.
* @format
*/
import angular, { IQService } from "angular"
import angular, { ITimeoutService } from "angular"
import _ from "lodash"
import settings from "@/settings"
import { regescape } from "@/util"
import { RootScope } from "@/root-scope.types"
import { LocationService } from "@/urlparams"
import { countAttrValues } from "@/backend/attr-values"
import { DataObject, GlobalFilterService, UpdateScope } from "./types"
import { CqpQuery, Condition } from "@/cqp_parser/cqp.types"
import { RecursiveRecord } from "@/backend/types/attr-values"
import { Attribute } from "@/settings/config.types"

export type GlobalFilterService = {
valueChange: () => void
}

type StoredFilterValues = Record<string, string[]>

// Data service for the global filter in korp
Expand All @@ -25,70 +28,54 @@ type StoredFilterValues = Record<string, string[]>
// diretives calls registerScope to register for updates
// service calls scope.update() when changes occur
angular.module("korpApp").factory("globalFilterService", [
"$rootScope",
"$location",
"$q",
function ($rootScope: RootScope, $location: LocationService, $q: IQService): GlobalFilterService {
const scopes: UpdateScope[] = []

const notify = () => listenerDef.promise.then(() => scopes.map((scope) => scope.update(dataObj)))

// deferred for waiting for all directives to register
// TODO Move dataObj to root scope and let the component watch it
var listenerDef = $q.defer<never>()

/** Model of filter data. */
const dataObj: DataObject = {}

"$rootScope",
"$timeout",
function ($location: LocationService, $rootScope: RootScope, $timeout: ITimeoutService): GlobalFilterService {
/** Drilldown of values available for each attr. N-dimensional map where N = number of attrs. */
let currentData: RecursiveRecord<number> = {}

function initFilters(filters: Record<string, Attribute>) {
// Remove filters that are no more applicable
for (const attr in dataObj) {
if (!filters[attr]) {
delete dataObj[attr]
}
for (const attr in $rootScope.globalFilterData) {
if (!filters[attr]) delete $rootScope.globalFilterData[attr]
}

// Add new filters
for (const attr in filters) {
// Replace settings of existing filters but keep their values
if (dataObj[attr]) Object.assign(dataObj[attr], filters)
else {
// Add new filters
dataObj[attr] = {
value: [], // Selection empty by default
options: [], // Filled in updateData
attribute: filters[attr],
}
$rootScope.globalFilterData[attr] ??= {
attribute: filters[attr],
value: [], // Selection empty by default
options: [], // Filled in updateData
}
}
}

/** Fetch token counts keyed in multiple dimensions by the values of attributes */
async function getData(): Promise<void> {
const corpora = settings.corpusListing.getSelectedCorpora()
const attrs = Object.keys(dataObj)
const multiAttrs = attrs.filter((attr) => dataObj[attr].attribute.type === "set")
const attrs = Object.keys($rootScope.globalFilterData)
const multiAttrs = attrs.filter((attr) => $rootScope.globalFilterData[attr].attribute.type === "set")
currentData = corpora.length && attrs.length ? await countAttrValues(corpora, attrs, multiAttrs) : {}
updateData()
$timeout(() => updateData())
}

// when user selects an attribute, update all possible filter values and counts
function updateData() {
function collectAndSum(
filters: string[],
attrs: string[],
elements: RecursiveRecord<number>,
parentSelected: boolean
): [number, boolean] {
const filter = filters[0]
const attr = attrs[0]
const filter = $rootScope.globalFilterData[attr]
let sum = 0
const values: string[] = []
let include = false
for (let value in elements) {
var childCount: number
const child = elements[value]
const selected = !dataObj[filter].value.length || dataObj[filter].value.includes(value)
const selected = !filter.value.length || filter.value.includes(value)

// filter of any parent values that do not support the child values
include = include || selected
Expand All @@ -97,11 +84,11 @@ angular.module("korpApp").factory("globalFilterService", [
childCount = child
include = true
} else {
;[childCount, include] = collectAndSum(_.tail(filters), child, parentSelected && selected)
;[childCount, include] = collectAndSum(_.tail(attrs), child, parentSelected && selected)
}

const countDisplay = include && parentSelected ? childCount : 0
dataObj[filter].options.push([value, countDisplay])
filter.options.push([value, countDisplay])

if (selected && include) {
sum += childCount
Expand All @@ -114,23 +101,21 @@ angular.module("korpApp").factory("globalFilterService", [
}

// reset all filters
for (const attr in dataObj) dataObj[attr].options = []
for (const filter of Object.values($rootScope.globalFilterData)) filter.options = []

// recursively decide the counts of all values
collectAndSum(Object.keys(dataObj), currentData, true)
collectAndSum(Object.keys($rootScope.globalFilterData), currentData, true)

// merge duplicate child values
for (const attr in dataObj) {
for (const filter of Object.values($rootScope.globalFilterData)) {
// Sum the counts of duplicate values
const options: Record<string, number> = {}
for (const [value, count] of dataObj[attr].options) {
for (const [value, count] of filter.options) {
options[value] ??= 0
options[value] += count
}
// Cast back to list and sort alphabetically
dataObj[attr].options = Object.entries(options).sort((a, b) =>
a[0].localeCompare(b[0], $rootScope.lang)
)
filter.options = Object.entries(options).sort((a, b) => a[0].localeCompare(b[0], $rootScope.lang))
}
}

Expand All @@ -142,14 +127,14 @@ angular.module("korpApp").factory("globalFilterService", [
const parsedFilter: StoredFilterValues = JSON.parse(atob(globalFilter))

// Copy values from param, reset filters not in param
for (const attr in dataObj) {
dataObj[attr].value = parsedFilter[attr] || []
for (const attr in $rootScope.globalFilterData) {
$rootScope.globalFilterData[attr].value = parsedFilter[attr] || []
}
}

/** Build a CQP token object of AND-combined conditions from active filters. */
function makeCqp(): CqpQuery {
const andArray: Condition[][] = Object.entries(dataObj).map(([attr, filter]) =>
const andArray: Condition[][] = Object.entries($rootScope.globalFilterData).map(([attr, filter]) =>
filter.value.map((value) => ({
type: `_.${attr}`,
op: filter.attribute.type === "set" ? "contains" : "=",
Expand All @@ -163,7 +148,7 @@ angular.module("korpApp").factory("globalFilterService", [
/** Set url param from local data, as base64-encoded json. */
function updateLocation() {
const rep: StoredFilterValues = {}
Object.entries(dataObj).forEach(([attr, filter]) => {
Object.entries($rootScope.globalFilterData).forEach(([attr, filter]) => {
if (filter.value.length) rep[attr] = filter.value
})
if (!_.isEmpty(rep)) {
Expand All @@ -183,7 +168,6 @@ angular.module("korpApp").factory("globalFilterService", [
setFromLocation($location.search().global_filter)
getData()
updateLocation()
notify()
}
// Flag that the filter feature is ready.
$rootScope.globalFilterDef.resolve()
Expand All @@ -196,13 +180,6 @@ angular.module("korpApp").factory("globalFilterService", [
)

return {
registerScope(scope) {
scopes.push(scope)
// TODO this will not work with parallel mode since only one directive is used :(
if (scopes.length === 2) {
listenerDef.resolve()
}
},
valueChange() {
updateLocation()
updateData()
Expand Down
6 changes: 3 additions & 3 deletions app/scripts/global-filter/global-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import "./global-filter-service"
import { LangString } from "@/i18n/types"
import { RootScope } from "@/root-scope.types"
import { Attribute } from "@/settings/config.types"
import { GlobalFilterService } from "./global-filter-service"

type GlobalFilterController = IController & {
attr: string
attrDef: Attribute
attrValue: string[]
options: [string, number][]
lang: string
}

type GlobalFilterScope = IScope & {
Expand Down Expand Up @@ -88,7 +88,7 @@ angular.module("korpApp").component("globalFilter", {
"$rootScope",
"$scope",
"globalFilterService",
function ($rootScope: RootScope, $scope: GlobalFilterScope, globalFilterService) {
function ($rootScope: RootScope, $scope: GlobalFilterScope, globalFilterService: GlobalFilterService) {
const $ctrl = this as GlobalFilterController
// if scope.options.length > 20
// # TODO enable autocomplete
Expand All @@ -112,7 +112,7 @@ angular.module("korpApp").component("globalFilter", {
$ctrl.attrValue.push(value)
}
event.stopPropagation()
globalFilterService.valueChange($ctrl.attr)
globalFilterService.valueChange()
}

$scope.isSelected = (value: string) => $ctrl.attrValue.includes(value)
Expand Down
35 changes: 14 additions & 21 deletions app/scripts/global-filter/global-filters.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
/** @format */
import angular, { IController } from "angular"
import angular, { IScope } from "angular"
import _ from "lodash"
import { html } from "@/util"
import "./global-filter-service"
import { DataObject, UpdateScope } from "./types"
import "./global-filter"
import { RootScope } from "@/root-scope.types"
import { GlobalFilterService } from "./global-filter-service"

type GlobalFiltersController = IController & {
lang: string
}

type GlobalFiltersScope = UpdateScope & {
dataObj: DataObject
type GlobalFiltersScope = IScope & {
show: boolean
}

angular.module("korpApp").component("globalFilters", {
template: html`<div ng-if="show" class="mb-4">
<span class="font-bold"> {{ 'global_filter' | loc:$root.lang}}:</span>
<div class="inline-block">
<span ng-repeat="(attr, filter) in dataObj">
<span ng-repeat="(attr, filter) in $root.globalFilterData">
<global-filter
attr="attr"
attr-def="filter.attribute"
Expand All @@ -32,20 +28,17 @@ angular.module("korpApp").component("globalFilters", {
</div>`,
bindings: {},
controller: [
"$rootScope",
"$scope",
"globalFilterService",
function ($scope: GlobalFiltersScope, globalFilterService) {
const $ctrl = this as GlobalFiltersController

$ctrl.$onInit = () => {
globalFilterService.registerScope($scope)
$scope.dataObj = {}
}

$scope.update = (dataObj) => {
$scope.dataObj = dataObj
$scope.show = Object.keys(dataObj).length > 0
}
function ($rootScope: RootScope, $scope: GlobalFiltersScope, globalFilterService: GlobalFilterService) {
$rootScope.$watch(
"globalFilterData",
() => {
$scope.show = Object.keys($rootScope.globalFilterData).length > 0
},
true
)
},
],
})
20 changes: 0 additions & 20 deletions app/scripts/global-filter/types.ts

This file was deleted.

11 changes: 11 additions & 0 deletions app/scripts/root-scope.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { KorpQueryRequestOptions } from "./backend/kwic-proxy"
import { CqpQuery } from "./cqp_parser/cqp.types"
import { CorpusListing } from "./corpus_listing"
import { CompareResult, MapRequestResult } from "@/backend/backend"
import { Attribute } from "./settings/config.types"

/** Extends the Angular Root Scope interface with properties used by this app. */
export type RootScope = IRootScopeService & {
Expand All @@ -14,6 +15,8 @@ export type RootScope = IRootScopeService & {
val: string
} | null
extendedCQP: string | null
/** Filter data by attribute name */
globalFilterData: Record<string, FilterData>
globalFilter: CqpQuery | null
globalFilterDef: IDeferred<never>
simpleCQP?: string
Expand All @@ -29,6 +32,14 @@ export type RootScope = IRootScopeService & {
$on: (name: "corpuschooserchange", handler: (event: any, selected: string[]) => void) => void
}

export type FilterData = {
attribute: Attribute
/** Selected values */
value: string[]
/** Sorted list of options with counts */
options: [string, number][]
}

export type CompareTab = IPromise<CompareResult>

export type GraphTab = {
Expand Down

0 comments on commit 49b2a5e

Please sign in to comment.