Skip to content

Commit 2a5a6b4

Browse files
FIREFLY-1694: Restructure unit data constant & use alias in lookup
1 parent 767c9d8 commit 2a5a6b4

File tree

2 files changed

+184
-63
lines changed

2 files changed

+184
-63
lines changed

src/firefly/js/charts/dataTypes/SpectrumUnitConversion.js

Lines changed: 181 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*
22
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
33
*/
4-
import {get} from 'lodash';
54
import {sprintf} from '../../externalSource/sprintf.js';
5+
import {WAVELENGTH_UNITS} from 'firefly/visualize/VisUtil';
66

77
/**
88
* Return true if 'from' unit can be converted to 'to'.
@@ -12,7 +12,9 @@ import {sprintf} from '../../externalSource/sprintf.js';
1212
* @returns {boolean}
1313
*/
1414
export function canUnitConv({from, to}) {
15-
return !! get(UnitXref, [from, to]);
15+
const fromKey = normalizeUnit(from);
16+
const toKey = normalizeUnit(to);
17+
return !!(fromKey && toKey && UnitXref?.[fromKey]?.[toKey]);
1618
}
1719

1820
/**
@@ -22,12 +24,14 @@ export function canUnitConv({from, to}) {
2224
* @param p.from from unit
2325
* @param p.to to unit
2426
* @param p.alias the name of this new column
25-
* @param p.colNames a list of all the columns in the table. This is used to format the expression so that column names are quoted correctly.
2627
* @param p.args any additional arguments used in the conversion formula.
2728
* @returns {string}
2829
*/
2930
export function getUnitConvExpr({cname, from, to, alias, args=[]}) {
30-
const formula = get(UnitXref, [from, to]);
31+
const fromKey = normalizeUnit(from);
32+
const toKey = normalizeUnit(to);
33+
const formula = UnitXref?.[fromKey]?.[toKey] ?? '';
34+
3135
let colOrExpr = cname;
3236
if (formula) {
3337
colOrExpr = sprintf(formula.replace(/(%([0-9]\$)?s)/g, '"$1"'), cname, ...args);
@@ -39,60 +43,62 @@ export function getUnitConvExpr({cname, from, to, alias, args=[]}) {
3943
/**
4044
* returns an object containing the axis label and an array of options for unit conversion.
4145
* @param {string} unit the unit to get the info for
42-
* @param {string} cname the name of the column being evaluated
46+
* @param {string} cname the name (or expression) of the column being evaluated
4347
* @returns {Object}
4448
*/
4549
export function getUnitInfo(unit, cname) {
46-
cname = cname?.match(/^"(.+)"$/)?.[1] ?? cname; // remove enclosing double-quotes if exists
47-
// TODO: account aliases in the lookup
48-
const meas = Object.values(UnitXref.measurement).find((m) => m?.options.find( (o) => o?.value === unit)) || {};
49-
let label = meas.label ? sprintf(meas.label, unit) : '';
50-
51-
// use column name (or expression) if couldn't recognize the unit in options of each measurement
52-
if (!label && cname) {
53-
label = cname + (unit ? `[${unit}]` : '');
50+
let options = [];
51+
let label = '';
52+
53+
const unitKey = normalizeUnit(unit);
54+
if (unitKey) {
55+
const measurementKey = UnitMetadata[unitKey]?.type;
56+
options = Object.entries(UnitMetadata)
57+
.filter(([,meta]) => meta?.type === measurementKey) // can only convert units of the same measurement type
58+
.map(([key, meta]) => ({value: key, label: meta.label || key}));
59+
60+
const unitLabel = UnitMetadata[unitKey]?.label || unitKey;
61+
const formattedLabel = Measurement[measurementKey]?.axisLabel;
62+
if (formattedLabel) label = sprintf(formattedLabel, unitLabel);
5463
}
55-
return {options: meas.options, label};
64+
else {
65+
cname = cname?.match(/^"(.+)"$/)?.[1] ?? cname; // remove enclosing double-quotes if exists
66+
if (cname) label = cname + (unit ? ` [${unit}]` : '');
67+
}
68+
69+
return {options, label};
5670
}
5771

5872

59-
/*
73+
/**
74+
* returns X axis label using the required information like unit, spectral frame options, redshift, etc.
75+
* @param {string} cname the name of the column being evaluated
76+
* @param {string} unit the unit to get the info for
77+
* @param {string} sfLabel the label of spectral frame selected
78+
* @param {string} redshiftLabel the label of redshift if rest frame, optional
79+
* @returns {string}
80+
*/
81+
export const getXLabel = (cname, unit, sfLabel, redshiftLabel='') => {
82+
const unitLabel = getUnitInfo(unit, cname).label;
83+
return `${sfLabel} ${unitLabel}${redshiftLabel ? `<br>(${redshiftLabel})` : ''}`;
84+
};
85+
86+
87+
88+
/**
89+
* @typedef {string} UnitKey
90+
* Unique identifier for a unit, used as a key in UnitXref and UnitMetadata.
91+
*/
92+
93+
/**
6094
Unit conversions, mapping FROM unit -> TO unit, where the formula is an SQL expression.
6195
The formula is a format string, similar to printf, where the %s is the column name of the value being converted.
6296
When argument index is needed, it can be referenced as %1$s, %2$s, %3$s, %4$s, etc.
63-
*/
6497
98+
Note: "outer" layer is the unit you *have*; "inner" layer is the unit you *want*
99+
@type {Object.<UnitKey, Object.<UnitKey, string>>}
100+
**/
65101
const UnitXref = {
66-
measurement: {
67-
frequency: {
68-
options: [{value:'Hz'}, {value:'KHz'}, {value:'MHz'}, {value:'GHz'}],
69-
label: '𝛎 [%s]'
70-
},
71-
wavelength: {
72-
options: [{value: 'A'}, {value: 'nm'}, {value:'um'}, {value: 'mm'}, {value:'cm'}, {value:'m'}],
73-
label: 'λ [%s]'
74-
},
75-
flux_density_frequency: {
76-
options: [{value:'W/m^2/Hz'}, {value:'erg/s/cm^2/Hz'}, {value:'Jy'}],
77-
label: 'F𝛎 [%s]'
78-
},
79-
flux_density_wavelength: {
80-
options: [{value:'W/m^2/um'}, {value:'erg/s/cm^2/A'}],
81-
label: 'Fλ [%s]'
82-
},
83-
inband_flux: {
84-
options: [{value:'W/m^2'}, {value:'Jy*Hz'}],
85-
label: '𝛎*F𝛎 [%s]'
86-
}
87-
},
88-
89-
aliases: {
90-
//TODO: add Angstrom, dot product notations of flux density, etc.
91-
},
92-
93-
// Unit Conversions follow
94-
// "outer" layer is the unit you *have*; "inner" layer is the unit you *want*
95-
96102
// frequency -------------
97103
Hz : {
98104
Hz : '%s',
@@ -210,15 +216,134 @@ const UnitXref = {
210216
},
211217
};
212218

219+
213220
/**
214-
* returns X axis label using the required information like unit, spectral frame options, redshift, etc.
215-
* @param {string} cname the name of the column being evaluated
216-
* @param {string} unit the unit to get the info for
217-
* @param {string} sfLabel the label of spectral frame selected
218-
* @param {string} redshiftLabel the label of redshift if rest frame, optional
219-
* @returns {string}
221+
* @typedef {string} MeasurementKey - unique identifier for a measurement type, used as a key in Measurement.
220222
*/
221-
export const getXLabel = (cname, unit, sfLabel, redshiftLabel='') => {
222-
const unitLabel = getUnitInfo(unit, cname).label;
223-
return `${sfLabel} ${unitLabel}${redshiftLabel ? `<br>(${redshiftLabel})` : ''}`;
224-
};
223+
224+
/**
225+
* @typedef {Object} Measurement
226+
* @property {MeasurementKey} key - the key of the measurement type
227+
* @property {string} value - the value of the measurement type
228+
* @property {string} axisLabel - the label for the axis, formatted with a placeholder for the unit
229+
*/
230+
231+
/**Type of measurements by which units are grouped.
232+
* @type {Object.<MeasurementKey, Measurement>}
233+
*/
234+
const Measurement = {
235+
NU: {key: 'NU', value: 'frequency', axisLabel: '𝛎 [%s]'},
236+
LAMBDA: {key: 'LAMBDA', value: 'wavelength', axisLabel: 'λ [%s]'},
237+
F_NU: {key: 'F_NU', value: 'flux_density_frequency', axisLabel: 'F𝛎 [%s]'},
238+
F_LAMBDA: {key: 'F_LAMBDA', value: 'flux_density_wavelength', axisLabel: 'Fλ [%s]'},
239+
F: {key: 'F', value: 'inband_flux', axisLabel: '𝛎·F𝛎 [%s]'},
240+
};
241+
242+
243+
/**
244+
* @typedef {Object} UnitMetadata
245+
* @property {MeasurementKey} type - the type of measurement this unit belongs to, one of the keys in `Measurement`.
246+
* @property {string} label - the label for the unit, used in dropdown options and axis labels. If undefined, the key
247+
* is used as the label.
248+
* @property {Array<string>} aliases - an object containing aliases for the unit, used for case-insensitive matching. If
249+
* undefined, only the key (and label) is used for matching.
250+
*/
251+
252+
/**
253+
* Metadata of each unit, including its type, label, and aliases. Keys are the same as in UnitXref.
254+
* @type {Object.<UnitKey, UnitMetadata>}
255+
* **/
256+
const UnitMetadata = {
257+
// frequency -------------
258+
Hz : {
259+
type: Measurement.NU.key,
260+
},
261+
KHz : {
262+
type: Measurement.NU.key,
263+
},
264+
MHz : {
265+
type: Measurement.NU.key,
266+
},
267+
GHz : {
268+
type: Measurement.NU.key,
269+
},
270+
// wavelength -------------
271+
A : {
272+
type: Measurement.LAMBDA.key,
273+
label: WAVELENGTH_UNITS.angstrom.symbol,
274+
aliases: ['angstrom', 'angstroms'] //case-insensitive
275+
},
276+
nm : {
277+
type: Measurement.LAMBDA.key,
278+
},
279+
um : {
280+
type: Measurement.LAMBDA.key,
281+
label: WAVELENGTH_UNITS.um.symbol,
282+
aliases: ['micron', 'microns'] //case-insensitive
283+
},
284+
mm : {
285+
type: Measurement.LAMBDA.key,
286+
},
287+
cm : {
288+
type: Measurement.LAMBDA.key,
289+
},
290+
m : {
291+
type: Measurement.LAMBDA.key,
292+
},
293+
// flux density in frequency space -------------
294+
'W/m^2/Hz' : {
295+
type: Measurement.F_NU.key,
296+
label: 'W/m²/Hz',
297+
aliases: [], //TODO: generate aliases in dot product notation with **, as it is with ** for powers
298+
},
299+
'erg/s/cm^2/Hz' : {
300+
type: Measurement.F_NU.key,
301+
label: 'erg/s/cm²/Hz',
302+
},
303+
Jy : {
304+
type: Measurement.F_NU.key,
305+
},
306+
// flux density in wavelength space -------------
307+
'erg/s/cm^2/A' : {
308+
type: Measurement.F_LAMBDA.key,
309+
label: `erg/s/cm²/${WAVELENGTH_UNITS.angstrom.symbol}`,
310+
},
311+
'W/m^2/um' : {
312+
type: Measurement.F_LAMBDA.key,
313+
label: `W/m²/${WAVELENGTH_UNITS.um.symbol}`,
314+
},
315+
// inband flux (independent of frequency or wavelength) -------------
316+
'W/m^2' : {
317+
type: Measurement.F.key,
318+
label: 'W/m²',
319+
},
320+
'erg/s/cm^2' : {
321+
type: Measurement.F.key,
322+
label: 'erg/s/cm²',
323+
},
324+
'Jy*Hz' : {
325+
type: Measurement.F.key,
326+
label: 'Jy·Hz',
327+
},
328+
};
329+
330+
331+
/**
332+
* Maps any unit value/label/alias back to a key in UnitXref (and UnitMetadata).
333+
* @param u {string} - the unit to normalize
334+
* @return {UnitKey|null} - the key in UnitXref if found, otherwise null
335+
*/
336+
function normalizeUnit(u) {
337+
if (!u) return null;
338+
// u is already a key in UnitXref
339+
if (UnitXref[u]) return u;
340+
341+
for (const [key, meta] of Object.entries(UnitMetadata)) {
342+
// u is a case-insensitive alias of a key in UnitXref
343+
if (meta?.aliases?.some((alias) => alias.toLowerCase() === u.toLowerCase())) return key;
344+
345+
// u is a label of a key in UnitXref
346+
if (meta?.label === u) return key;
347+
}
348+
return null;
349+
}

src/firefly/js/charts/ui/options/SpectrumOptions.jsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {fieldReducer, submitChangesScatter, ScatterCommonOptions, useScatterInpu
1414
import {VALUE_CHANGE} from '../../../fieldGroup/FieldGroupCntlr.js';
1515
import {updateSet, toBoolean} from '../../../util/WebUtil.js';
1616
import {isSpectralOrder, getChartProps} from '../../ChartUtil.js';
17-
import {LayoutOptions, useBasicOptions} from './BasicOptions.jsx';
17+
import {LayoutOptions} from './BasicOptions.jsx';
1818
import {getSpectrumProps} from '../../dataTypes/FireflySpectrum.js';
1919
import {getFieldVal, revalidateFields} from 'firefly/fieldGroup/FieldGroupUtils';
2020
import {isFloat} from 'firefly/util/Validate';
@@ -36,7 +36,6 @@ export function SpectrumOptions ({activeTrace:pActiveTrace, tbl_id:ptbl_id, char
3636

3737
const {Xunit, Yunit, SpectralFrame} = useSpectrumInputs({activeTrace, tbl_id, chartId, groupKey});
3838
const {UseSpectrum, X, Xmax, Xmin, Y, Ymax, Ymin, Yerrors, Xerrors, Mode} = useScatterInputs({activeTrace, tbl_id, chartId, groupKey});
39-
const {XaxisTitle, YaxisTitle} = useBasicOptions({activeTrace, tbl_id, chartId, groupKey});
4039

4140
const reducerFunc = spectrumReducer({chartId, activeTrace, tbl_id});
4241
reducerFunc.ver = chartId+activeTrace+tbl_id;
@@ -70,10 +69,7 @@ export function SpectrumOptions ({activeTrace:pActiveTrace, tbl_id:ptbl_id, char
7069
</Stack>
7170
<CollapsibleGroup>
7271
<ScatterCommonOptions{...{activeTrace, tbl_id, chartId, groupKey}}/>
73-
<LayoutOptions {...{activeTrace, tbl_id, chartId, groupKey}}
74-
XaxisTitle={() => <XaxisTitle readonly={true}/>}
75-
YaxisTitle={() => <YaxisTitle readonly={true}/>}
76-
/>
72+
<LayoutOptions {...{activeTrace, tbl_id, chartId, groupKey}}/>
7773
</CollapsibleGroup>
7874
</Stack>
7975
</FieldGroup>
@@ -289,7 +285,7 @@ function Units({activeTrace, value, axis, ...rest}) {
289285
const options = getUnitInfo(value)?.options;
290286
const unrecognizedTip = 'Cannot change these units because they could not be recognized.';
291287

292-
return options
288+
return options?.length > 1
293289
? <ListBoxInputField fieldKey={`fireflyData.${activeTrace}.${unitProp}`} initialState={{value}}
294290
{...{label, options, ...rest}}/>
295291
: <ReadOnlyField fieldKey={`fireflyData.${activeTrace}.${unitProp}`} value={value} label={label}

0 commit comments

Comments
 (0)