Skip to content

Commit 400de05

Browse files
committed
support to handle large nominal fields on x/y axes
1 parent 81a3cc0 commit 400de05

File tree

6 files changed

+87
-60
lines changed

6 files changed

+87
-60
lines changed

src/app/utils.tsx

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,13 @@ export function baseTableToExtTable(table: any[], derivedFields: FieldItem[], al
240240
}
241241

242242

243-
export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key in Channel]: EncodingItem; }, allFields: FieldItem[], workingTable: any[]) => {
243+
export const assembleVegaChart = (
244+
chartType: string,
245+
encodingMap: { [key in Channel]: EncodingItem; },
246+
conceptShelfItems: FieldItem[],
247+
workingTable: any[],
248+
maxNominalValues: number = 68
249+
) => {
244250

245251
if (chartType == "Table") {
246252
return ["Table", undefined];
@@ -250,7 +256,6 @@ export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key i
250256
//console.log(chartTemplate);
251257

252258
let vgObj = JSON.parse(JSON.stringify(chartTemplate.template));
253-
const baseTableSchemaObj: any = {};
254259

255260
for (const [channel, encoding] of Object.entries(encodingMap)) {
256261

@@ -260,30 +265,8 @@ export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key i
260265
encodingObj["scale"] = {"type": "sqrt", "zero": true};
261266
}
262267

263-
const field = encoding.fieldID ? _.find(allFields, (f) => f.id === encoding.fieldID) : undefined;
268+
const field = encoding.fieldID ? _.find(conceptShelfItems, (f) => f.id === encoding.fieldID) : undefined;
264269
if (field) {
265-
//console.log(field)
266-
// the synthesizer only need to see base table schema
267-
let baseFields = (field.source == "derived" ?
268-
(field.transform as ConceptTransformation).parentIDs.map((parentID) => allFields.find((f) => f.id == parentID) as FieldItem)
269-
: [field]);
270-
271-
for (let baseField of baseFields) {
272-
if (Object.keys(baseTableSchemaObj).includes(baseField.name)) {
273-
continue;
274-
}
275-
baseTableSchemaObj[baseField.name] = {
276-
channel,
277-
dtype: getDType(baseField.type, workingTable.map(r => r[baseField.name])),
278-
name: baseField.name,
279-
original: baseField.source == "original",
280-
// domain: {
281-
// values: [...new Set(baseField.domain.values)],
282-
// is_complete: baseField.domain.isComplete
283-
// },
284-
};
285-
}
286-
287270
// create the encoding
288271
encodingObj["field"] = field.name;
289272
encodingObj["type"] = getDType(field.type, workingTable.map(r => r[field.name]));
@@ -426,16 +409,8 @@ export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key i
426409
vgObj = chartTemplate.postProcessor(vgObj, workingTable);
427410
}
428411

429-
// console.log(JSON.stringify(vgObj))
430-
431-
return [vgObj, baseTableSchemaObj];
432-
}
433-
434-
export const assembleChart = (chart: Chart, conceptShelfItems: FieldItem[], dataValues: any[]) => {
435-
436-
let vgSpec: any = instantiateVegaTemplate(chart.chartType, chart.encodingMap, conceptShelfItems, dataValues)[0];
437-
438-
let values = JSON.parse(JSON.stringify(dataValues));
412+
// this is the data that will be assembled into the vega chart
413+
let values = JSON.parse(JSON.stringify(workingTable));
439414
values = values.map((r: any) => {
440415
let keys = Object.keys(r);
441416
let temporalKeys = keys.filter((k: string) => conceptShelfItems.some(concept => concept.name == k && (concept.type == "date" || concept.semanticType == "Year")));
@@ -444,9 +419,75 @@ export const assembleChart = (chart: Chart, conceptShelfItems: FieldItem[], data
444419
}
445420
return r;
446421
})
447-
return {...vgSpec, data: {values: values}}
422+
423+
// Handle nominal axes with many entries
424+
for (const channel of ['x', 'y']) {
425+
const encoding = vgObj.encoding?.[channel];
426+
if (encoding?.type === 'nominal') {
427+
const fieldName = encoding.field;
428+
const uniqueValues = [...new Set(workingTable.map(r => r[fieldName]))];
429+
430+
if (uniqueValues.length > maxNominalValues) {
431+
const oppositeChannel = channel === 'x' ? 'y' : 'x';
432+
const oppositeEncoding = vgObj.encoding?.[oppositeChannel];
433+
434+
let valuesToKeep: any[];
435+
if (oppositeEncoding?.type === 'quantitative') {
436+
// Sort by the quantitative field and take top maxNominalValues
437+
const quantField = oppositeEncoding.field;
438+
valuesToKeep = uniqueValues
439+
.map(val => ({
440+
value: val,
441+
sum: workingTable
442+
.filter(r => r[fieldName] === val)
443+
.reduce((sum, r) => sum + (r[quantField] || 0), 0)
444+
}))
445+
.sort((a, b) => b.sum - a.sum)
446+
.slice(0, maxNominalValues)
447+
.map(v => v.value);
448+
} else {
449+
// If no quantitative axis, just take first maxNominalValues
450+
valuesToKeep = uniqueValues.slice(0, maxNominalValues);
451+
}
452+
453+
// Filter the working table
454+
const omittedCount = uniqueValues.length - maxNominalValues;
455+
const placeholder = `...${omittedCount} items omitted`;
456+
values = values.filter((row: any) => valuesToKeep.includes(row[fieldName]));
457+
458+
// Add text formatting configuration
459+
if (!encoding.axis) {
460+
encoding.axis = {};
461+
}
462+
encoding.axis.labelColor = {
463+
condition: {
464+
test: `datum.label == '${placeholder}'`,
465+
value: "#999999"
466+
},
467+
value: "#000000" // default color for other labels
468+
};
469+
encoding.axis.labelFont = {
470+
condition: {
471+
test: `datum.label == '${placeholder}'`,
472+
value: "italic"
473+
},
474+
value: "normal" // default font style for other labels
475+
};
476+
477+
// Add placeholder to domain
478+
if (!encoding.scale) {
479+
encoding.scale = {};
480+
}
481+
encoding.scale.domain = [...valuesToKeep, placeholder]
482+
}
483+
}
484+
}
485+
486+
return {...vgObj, data: {values: values}}
448487
}
449488

489+
490+
450491
export const adaptChart = (chart: Chart, targetTemplate: ChartTemplate) => {
451492

452493
let discardedChannels = Object.entries(chart.encodingMap).filter(([ch, enc]) => {

src/views/DataThread.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { VegaLite } from 'react-vega'
2424
import '../scss/VisualizationView.scss';
2525
import { useDispatch, useSelector } from 'react-redux';
2626
import { DataFormulatorState, dfActions } from '../app/dfSlice';
27-
import { assembleChart, baseTableToExtTable, getTriggers } from '../app/utils';
27+
import { assembleVegaChart, baseTableToExtTable, getTriggers } from '../app/utils';
2828
import { Chart, DictTable, EncodingItem, Trigger } from "../components/ComponentType";
2929

3030
import DeleteIcon from '@mui/icons-material/Delete';
@@ -367,7 +367,7 @@ export const DataThread: FC<{}> = function ({ }) {
367367
}
368368

369369
// prepare the chart to be rendered
370-
let assembledChart = assembleChart(chart, conceptShelfItems, extTable);
370+
let assembledChart = assembleVegaChart(chart.chartType, chart.encodingMap, conceptShelfItems, extTable, 20);
371371
assembledChart["background"] = "transparent";
372372

373373
// Temporary fix, down sample the dataset

src/views/DerivedDataDialog.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020

2121
import React from 'react';
2222

23-
import { baseTableToExtTable, instantiateVegaTemplate } from '../app/utils';
23+
import { baseTableToExtTable, assembleVegaChart } from '../app/utils';
2424
import { Chart } from '../components/ComponentType';
2525
import { findBaseFields } from './ViewUtils';
2626
import { useSelector } from 'react-redux';
@@ -74,10 +74,7 @@ export const DerivedDataDialog: FC<DerivedDataDialogProps> = function DerivedDat
7474
let toDeriveFields = derivedFields.filter(f => f.name != "").filter(f => findBaseFields(f, conceptShelfItems).every(f2 => table.names.includes(f2.name)))
7575
let extTable = baseTableToExtTable(JSON.parse(JSON.stringify(table.rows)), toDeriveFields, conceptShelfItems);
7676

77-
let vgSpec: any = instantiateVegaTemplate(chart.chartType, chart.encodingMap, conceptShelfItems, extTable)[0];
78-
79-
80-
let assembledChart = {...vgSpec, data: {values: JSON.parse(JSON.stringify(extTable))}};
77+
let assembledChart: any = assembleVegaChart(chart.chartType, chart.encodingMap, conceptShelfItems, extTable)[0];
8178
assembledChart["background"] = "transparent";
8279
// chart["autosize"] = {
8380
// "type": "fit",

src/views/EncodingShelfCard.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ let selectBaseTables = (activeFields: FieldItem[], conceptShelfItems: FieldItem[
8989
baseTables.push(...tablesToAdd.filter(t => !baseTables.map(t2 => t2.id).includes(t.id)));
9090
}
9191

92-
console.log("selectBaseTables baseTables");
93-
console.log(baseTables);
94-
9592
return baseTables;
9693
}
9794

@@ -430,19 +427,12 @@ export const EncodingShelfCard: FC<EncodingShelfCardProps> = function ({ chartId
430427
if (mode == "formulate" && currentTable.derive?.dialog) {
431428
let sourceTableIds = currentTable.derive?.source;
432429

433-
console.log("sourceTableIds ---- and ---- baseTableIds");
434-
console.log(sourceTableIds);
435-
console.log(actionTableIds);
436-
437430
// Compare if source and base table IDs are different
438431
if (!sourceTableIds.every(id => actionTableIds.includes(id)) ||
439432
!actionTableIds.every(id => sourceTableIds.includes(id))) {
440433

441434
let additionalMessages = currentTable.derive.dialog;
442435

443-
console.log("in here");
444-
console.log(additionalMessages);
445-
446436
// in this case, because table ids has changed, we need to use the additional messages and reformulate
447437
messageBody = JSON.stringify({
448438
token: token,
@@ -490,8 +480,7 @@ export const EncodingShelfCard: FC<EncodingShelfCardProps> = function ({ chartId
490480
.then((data) => {
491481

492482
dispatch(dfActions.changeChartRunningStatus({chartId, status: false}))
493-
console.log(data);
494-
console.log(token);
483+
495484
if (data.results.length > 0) {
496485
if (data["token"] == token) {
497486
let candidates = data["results"].filter((item: any) => {

src/views/EncodingShelfThread.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import '../scss/EncodingShelf.scss';
2727
import { createDictTable, DictTable } from "../components/ComponentType";
2828
import embed from 'vega-embed';
2929

30-
import { getTriggers, getUrls, assembleChart, resolveChartFields } from '../app/utils';
30+
import { getTriggers, getUrls, assembleVegaChart, resolveChartFields } from '../app/utils';
3131

3232
import { getChartTemplate } from '../components/ChartTemplates';
3333
import { chartAvailabilityCheck, generateChartSkeleton } from './VisualizationView';
@@ -74,7 +74,7 @@ export let ChartElementFC: FC<{chart: Chart, tableRows: any[], boxWidth?: number
7474
// }
7575

7676
// prepare the chart to be rendered
77-
let assembledChart = assembleChart(chart, conceptShelfItems, tableRows);
77+
let assembledChart = assembleVegaChart(chart.chartType, chart.encodingMap, conceptShelfItems, tableRows);
7878
assembledChart["background"] = "transparent";
7979
// chart["autosize"] = {
8080
// "type": "fit",

src/views/VisualizationView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import AnimateOnChange from 'react-animate-on-change'
3636
import '../scss/VisualizationView.scss';
3737
import { useDispatch, useSelector } from 'react-redux';
3838
import { DataFormulatorState, dfActions } from '../app/dfSlice';
39-
import { assembleChart, baseTableToExtTable } from '../app/utils';
39+
import { assembleVegaChart, baseTableToExtTable } from '../app/utils';
4040
import { Chart, EncodingItem, EncodingMap, FieldItem } from '../components/ComponentType';
4141
import { DictTable } from "../components/ComponentType";
4242

@@ -311,7 +311,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[],
311311

312312
element = <Box id={id} key={`focused-chart`} ></Box>
313313

314-
let assembledChart = assembleChart(chart, conceptShelfItems, extTable);
314+
let assembledChart = assembleVegaChart(chart.chartType, chart.encodingMap, conceptShelfItems, extTable);
315315
assembledChart['resize'] = true;
316316

317317
embed('#' + id, { ...assembledChart }, { actions: true, renderer: "svg" }).then(function (result) {
@@ -796,7 +796,7 @@ export const VisualizationViewFC: FC<VisPanelProps> = function VisualizationView
796796
key="skeleton" onClick={setIndexFunc}>{generateChartSkeleton(chartTemplate?.icon)}</Box>;
797797
}
798798

799-
let assembledChart = assembleChart(chart, conceptShelfItems, extTable);
799+
let assembledChart = assembleVegaChart(chart.chartType, chart.encodingMap, conceptShelfItems, extTable);
800800

801801
const id = `chart-element-${index}`;
802802

0 commit comments

Comments
 (0)