diff --git a/src/__demo__/PivotTable.aggregate.stories.js b/src/__demo__/PivotTable.aggregate.stories.js
index d3b558eaf..3602c89d5 100644
--- a/src/__demo__/PivotTable.aggregate.stories.js
+++ b/src/__demo__/PivotTable.aggregate.stories.js
@@ -470,6 +470,54 @@ DeepFilter.story = {
name: 'deep - filter',
}
+/* This layout has a filter the lib can read, so it derives "Eastern Area".
+ * A caller that would rather format the line itself passes `filterText`.
+ * Whether there is a row at all stays the layout's call either way, which
+ * the third mode shows. */
+const filterTextModes = [
+ { label: 'Derived by the lib' },
+ { label: 'Supplied by the caller', filterText: 'Age: 5 - 10, Female' },
+ {
+ label: 'Supplied, but the layout has no filters',
+ filterText: 'Age: 5 - 10, Female',
+ filters: [],
+ },
+]
+
+export const DeepSuppliedFilterText = (_, { pivotTableOptions }) => {
+ const [modeIndex, setModeIndex] = useState(0)
+ const mode = filterTextModes[modeIndex]
+ const visualization = {
+ ...deepWithFiltersVisualization,
+ ...visualizationReset,
+ ...pivotTableOptions,
+ showDimensionLabels: false,
+ filters: mode.filters ?? deepWithFiltersVisualization.filters,
+ }
+ return (
+
+ {filterTextModes.map((each, index) => (
+
+ ))}
+
+
+ )
+}
+
+DeepSuppliedFilterText.storyName = 'deep - filter text supplied by the caller'
+
export const DeepTitleSubtitleFilter = (_, { pivotTableOptions }) => {
const visualization = {
...deepVisualization,
diff --git a/src/components/PivotTable/PivotTable.js b/src/components/PivotTable/PivotTable.js
index ab5c74ca0..b480bc784 100644
--- a/src/components/PivotTable/PivotTable.js
+++ b/src/components/PivotTable/PivotTable.js
@@ -13,6 +13,7 @@ const PivotTable = ({
visualization,
data,
legendSets,
+ filterText,
renderCounter,
onToggleContextualMenu,
}) => {
@@ -20,8 +21,14 @@ const PivotTable = ({
const { width, height } = useParentSize(containerRef, renderCounter)
const engine = useMemo(
- () => new PivotTableEngine(visualization, data, legendSets),
- [visualization, data, legendSets]
+ () =>
+ new PivotTableEngine({
+ visualization,
+ data,
+ legendSets,
+ filterText,
+ }),
+ [visualization, data, legendSets, filterText]
)
const { sortBy, onSortByColumn } = useSortableColumns(engine)
@@ -58,6 +65,7 @@ const PivotTable = ({
PivotTable.propTypes = {
data: PropTypes.object.isRequired,
visualization: PropTypes.object.isRequired,
+ filterText: PropTypes.string,
legendSets: PropTypes.arrayOf(PropTypes.object),
renderCounter: PropTypes.number,
onToggleContextualMenu: PropTypes.func,
diff --git a/src/components/PivotTable/PivotTableTitleRows.js b/src/components/PivotTable/PivotTableTitleRows.js
index b29439261..6376a7e95 100644
--- a/src/components/PivotTable/PivotTableTitleRows.js
+++ b/src/components/PivotTable/PivotTableTitleRows.js
@@ -25,10 +25,13 @@ export const PivotTableTitleRows = ({ clippingResult, width }) => {
) : null}
{engine.visualization.filters?.length ? (
diff --git a/src/components/PivotTable/__tests__/PivotTableTitleRows.spec.js b/src/components/PivotTable/__tests__/PivotTableTitleRows.spec.js
new file mode 100644
index 000000000..b045d9f0f
--- /dev/null
+++ b/src/components/PivotTable/__tests__/PivotTableTitleRows.spec.js
@@ -0,0 +1,101 @@
+import { render, screen } from '@testing-library/react'
+import React from 'react'
+import deepWithFiltersData from '../../../__demo__/data/aggregate/deepWithFilters.data.json'
+import deepWithFiltersVisualization from '../../../__demo__/data/aggregate/deepWithFilters.visualization.json'
+import PivotTable from '../PivotTable.js'
+
+/* The engine measures every cell against a canvas 2d context, which jsdom
+ * does not implement. */
+jest.mock('../../../modules/pivotTable/measureText.js', () => ({
+ measureTextWithWrapping: () => ({ width: 100, height: 20 }),
+}))
+
+/* The container renders nothing at all until it has a size, and jsdom reports
+ * every element as 0 x 0. */
+jest.mock('../../../modules/pivotTable/useParentSize.js', () => ({
+ useParentSize: () => ({ width: 800, height: 600 }),
+}))
+
+/* `deepWithFilters` filters on an org unit group set, which `getFilterText`
+ * resolves through `metaData.dimensions` to this name. */
+const DERIVED_FILTER_TEXT = 'Eastern Area'
+
+const visualization = {
+ ...deepWithFiltersVisualization,
+ title: 'Hello',
+ subtitle: 'Goodbye',
+}
+
+const renderTable = (props) =>
+ render(
+
+ )
+
+const titleRowTexts = () =>
+ screen
+ .queryAllByTestId('visualization-title')
+ .map((element) => element.textContent)
+
+describe('PivotTable title rows', () => {
+ it('derives the filter row from the layout when no text is supplied', () => {
+ renderTable()
+
+ expect(titleRowTexts()).toEqual([
+ 'Hello',
+ 'Goodbye',
+ DERIVED_FILTER_TEXT,
+ ])
+ })
+
+ it('renders no filter row when the layout has no filters', () => {
+ renderTable({ visualization: { ...visualization, filters: [] } })
+
+ expect(titleRowTexts()).toEqual(['Hello', 'Goodbye'])
+ })
+
+ it('renders the supplied text in place of the derived one', () => {
+ renderTable({ filterText: 'Age 5 - 10, Female' })
+
+ expect(titleRowTexts()).toEqual([
+ 'Hello',
+ 'Goodbye',
+ 'Age 5 - 10, Female',
+ ])
+ })
+
+ /* Whether there is a row remains the layout's call - supplying the text
+ * says what goes in the row, not whether there is one. */
+ it('does not render a supplied text the layout has no row for', () => {
+ renderTable({
+ visualization: { ...visualization, filters: [] },
+ filterText: 'Age 5 - 10, Female',
+ })
+
+ expect(titleRowTexts()).toEqual(['Hello', 'Goodbye'])
+ })
+
+ /* An empty string is a caller with nothing to say, not a caller asking
+ * for an empty row. */
+ it('derives the filter row for an empty supplied text', () => {
+ renderTable({ filterText: '' })
+
+ expect(titleRowTexts()).toEqual([
+ 'Hello',
+ 'Goodbye',
+ DERIVED_FILTER_TEXT,
+ ])
+ })
+
+ it('leaves the title and subtitle to the visualization', () => {
+ renderTable({
+ visualization: { ...visualization, hideSubtitle: true },
+ filterText: 'Age 5 - 10',
+ })
+
+ expect(titleRowTexts()).toEqual(['Hello', 'Age 5 - 10'])
+ })
+})
diff --git a/src/modules/pivotTable/PivotTableEngine.js b/src/modules/pivotTable/PivotTableEngine.js
index cdcc8257c..d2125c7e3 100644
--- a/src/modules/pivotTable/PivotTableEngine.js
+++ b/src/modules/pivotTable/PivotTableEngine.js
@@ -298,7 +298,7 @@ export class PivotTableEngine {
columnMap = []
accumulators = { rows: {} }
- constructor(visualization, data, legendSets) {
+ constructor({ visualization, data, legendSets, filterText }) {
this.visualization = Object.assign(
{},
defaultVisualizationProps,
@@ -336,6 +336,9 @@ export class PivotTableEngine {
? visualization.fixRowHeaders
: false,
cumulativeValues: visualization.cumulativeValues,
+ /* If visualization.filters is populated and this option is present,
+ * this text is used instead of the text derived from `visualization.filters` */
+ filterText,
}
this.adaptiveClippingController = new AdaptiveClippingController(this)
diff --git a/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js b/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js
index 56b5e8b3b..e854d2093 100644
--- a/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js
+++ b/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js
@@ -57,10 +57,10 @@ const cellValues = (engine) =>
describe('PivotTableEngine org unit hierarchy', () => {
it('applies the hierarchy to a bare `ou` dimension', () => {
- const engine = new PivotTableEngine(
- buildVisualization('ou'),
- buildData('ou')
- )
+ const engine = new PivotTableEngine({
+ visualization: buildVisualization('ou'),
+ data: buildData('ou'),
+ })
expect(rowHierarchies(engine)).toEqual([
['Sierra Leone', 'Northern Province', 'Bombali'],
@@ -70,10 +70,10 @@ describe('PivotTableEngine org unit hierarchy', () => {
it('applies the hierarchy to a stage-qualified `ou` dimension', () => {
const dimension = `${STAGE}.ou`
- const engine = new PivotTableEngine(
- buildVisualization(dimension),
- buildData(dimension)
- )
+ const engine = new PivotTableEngine({
+ visualization: buildVisualization(dimension),
+ data: buildData(dimension),
+ })
expect(rowHierarchies(engine)).toEqual([
['Sierra Leone', 'Northern Province', 'Bombali'],
@@ -82,10 +82,10 @@ describe('PivotTableEngine org unit hierarchy', () => {
})
it('applies the hierarchy to an `enrollmentou` dimension', () => {
- const engine = new PivotTableEngine(
- buildVisualization('enrollmentou'),
- buildData('enrollmentou')
- )
+ const engine = new PivotTableEngine({
+ visualization: buildVisualization('enrollmentou'),
+ data: buildData('enrollmentou'),
+ })
expect(rowHierarchies(engine)).toEqual([
['Sierra Leone', 'Northern Province', 'Bombali'],
@@ -94,10 +94,13 @@ describe('PivotTableEngine org unit hierarchy', () => {
})
it('leaves items untouched when showHierarchy is off', () => {
- const engine = new PivotTableEngine(
- { ...buildVisualization(`${STAGE}.ou`), showHierarchy: false },
- buildData(`${STAGE}.ou`)
- )
+ const engine = new PivotTableEngine({
+ visualization: {
+ ...buildVisualization(`${STAGE}.ou`),
+ showHierarchy: false,
+ },
+ data: buildData(`${STAGE}.ou`),
+ })
expect(rowHierarchies(engine)).toEqual([undefined, undefined])
expect(engine.getRowHeader(0)[0].uid).toBe(OU_A)
@@ -106,10 +109,10 @@ describe('PivotTableEngine org unit hierarchy', () => {
/* Sorting rewrites itemIds, which the row lookup resolves data rows
* through. If the two fall out of step every cell renders empty. */
it('keeps values aligned with the re-sorted rows', () => {
- const engine = new PivotTableEngine(
- buildVisualization(`${STAGE}.ou`),
- buildData(`${STAGE}.ou`)
- )
+ const engine = new PivotTableEngine({
+ visualization: buildVisualization(`${STAGE}.ou`),
+ data: buildData(`${STAGE}.ou`),
+ })
expect(rowNames(engine)).toEqual(['Bombali', 'Bo'])
expect(cellValues(engine)).toEqual(['2', '1'])
@@ -122,7 +125,10 @@ describe('PivotTableEngine org unit hierarchy', () => {
const data = buildData(dimension)
data.metaData.ouNameHierarchy = {}
- const engine = new PivotTableEngine(buildVisualization(dimension), data)
+ const engine = new PivotTableEngine({
+ visualization: buildVisualization(dimension),
+ data,
+ })
expect(rowNames(engine)).toEqual(['Bo', 'Bombali'])
expect(rowHierarchies(engine)).toEqual([undefined, undefined])
@@ -131,10 +137,10 @@ describe('PivotTableEngine org unit hierarchy', () => {
it('leaves non-org-unit dimensions untouched', () => {
const dimension = `${STAGE}.de1`
- const engine = new PivotTableEngine(
- buildVisualization(dimension),
- buildData(dimension)
- )
+ const engine = new PivotTableEngine({
+ visualization: buildVisualization(dimension),
+ data: buildData(dimension),
+ })
expect(rowHierarchies(engine)).toEqual([undefined, undefined])
})