Skip to content

Commit

Permalink
fix: correctly support timeFormatSpecifier in Vega-Lite
Browse files Browse the repository at this point in the history
- Introduce a common `type Format = string | TimeFormatSpecifier | Dict<unknown>;` and re-use everywhere.
- Replace bad
- Add examples
  • Loading branch information
kanitw committed Oct 11, 2024
1 parent 4098609 commit 0f0558b
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 32,250 deletions.
32,227 changes: 0 additions & 32,227 deletions build/vega-lite-schema.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions examples/specs/line_default_format.vl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Google's stock price over time.",
"data": {"url": "data/stocks.csv"},
"transform": [{"filter": "datum.symbol==='GOOG'"}, {"filter": {"timeUnit": "year", "field": "date", "oneOf": [2006, 2007]}}],
"width": 400,
"mark": "line",
"encoding": {
"x": {"field": "date", "type": "temporal"},
"y": {"field": "price", "type": "quantitative"}
}
}
20 changes: 20 additions & 0 deletions examples/specs/line_override_dynamic_format.vl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Google's stock price over time.",
"data": {"url": "data/stocks.csv"},
"transform": [{"filter": "datum.symbol==='GOOG'"}, {"filter": {"timeUnit": "year", "field": "date", "oneOf": [2006, 2007]}}],
"width": 400,
"mark": "line",
"encoding": {
"x": {
"field": "date", "type": "temporal",
"axis": {
"format": {
"year": "%y",
"quarter": "%b"
}
}
},
"y": {"field": "price", "type": "quantitative"}
}
}
15 changes: 15 additions & 0 deletions examples/specs/line_override_static_format.vl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Google's stock price over time.",
"data": {"url": "data/stocks.csv"},
"transform": [{"filter": "datum.symbol==='GOOG'"}, {"filter": {"timeUnit": "year", "field": "date", "oneOf": [2006, 2007]}}],
"width": 400,
"mark": "line",
"encoding": {
"x": {
"field": "date", "type": "temporal",
"axis": {"format": "%Y %b"}
},
"y": {"field": "price", "type": "quantitative"}
}
}
6 changes: 3 additions & 3 deletions site/_data/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
vega: 5.28.0
vega-lite: 5.18.1
vega-embed: 6.25.0
vega: 5.30.0
vega-lite: 5.21.0
vega-embed: 6.26.0
vega-tooltip: 0.34.0
22 changes: 19 additions & 3 deletions site/docs/encoding/format.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,26 @@ Below, we override the default number formatting to use exponent notation set to

### Temporal Data

In the example below we format the axis label to only show the year.
#### Default

<span class="vl-example" data-name="line"></span>
By default, time fields may have dynamic time format that uses different formats depending on the granularity of the input date (e.g., if the tick's date lies on a year, month, date, hour, etc. boundary).

The format can also contain text.
<span class="vl-example" data-name="line_default_format"></span>

#### Specifying Dynamic Time Format

{:#dynamic-time-format}

We can override dynamic time format by setting `format` to an object where the keys are valid [Vega time units](https://vega.github.io/vega/docs/api/time/#time-units) (e.g., `year`, `month`, etc) and the values are [d3-time-format](https://d3js.org/d3-time-format#locale_format) specifier strings.

<span class="vl-example" data-name="line_override_dynamic_format"></span>

#### Specifying Static Time Format

If you prefer using a static time format, you can set `format` to a desired time format specifier string:

<span class="vl-example" data-name="line_override_static_format"></span>

Note that time format can also contain text.

<span class="vl-example" data-name="bar_yearmonth_custom_format"></span>
16 changes: 10 additions & 6 deletions src/channeldef.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Gradient, ScaleType, SignalRef, Text} from 'vega';
import {Gradient, ScaleType, SignalRef, Text, TimeFormatSpecifier} from 'vega';
import {isArray, isBoolean, isNumber, isString} from 'vega-util';
import {Aggregate, isAggregateOp, isArgmaxDef, isArgminDef, isCountingAggregateOp} from './aggregate';
import {Axis} from './axis';
Expand Down Expand Up @@ -399,20 +399,24 @@ export interface DatumDef<
// `F extends RepeatRef` probably should be `RepeatRef extends F` but there is likely a bug in TS.
}

export type Format = string | TimeFormatSpecifier | Dict<unknown>;

export interface FormatMixins {
/**
* When used with the default `"number"` and `"time"` format type, the text formatting pattern for labels of guides (axes, legends, headers) and text marks.
* The text format specifier for formatting number and date/time in labels of guides (axes, legends, headers) and text marks.
*
* If the format type is `"number"` (e.g., for quantitative fields), this is a D3's [number format pattern string](https://github.com/d3/d3-format#locale_format).
*
* - If the format type is `"number"` (e.g., for quantitative fields), this is D3's [number format pattern](https://github.com/d3/d3-format#locale_format).
* - If the format type is `"time"` (e.g., for temporal fields), this is D3's [time format pattern](https://github.com/d3/d3-time-format#locale_format).
* If the format type is `"time"` (e.g., for temporal fields), this is either:
* a) D3's [time format pattern](https://d3js.org/d3-time-format#locale_format) if you desire to set a static time format.
*
* See the [format documentation](https://vega.github.io/vega-lite/docs/format.html) for more examples.
* b) [dynamic time format specifier object](https://vega.github.io/vega-lite/docs/format.html#dynamic-time-format) if you desire to set a dynamic time format that uses different formats depending on the granularity of the input date (e.g., if the date lies on a year, month, date, hour, etc. boundary).
*
* When used with a [custom `formatType`](https://vega.github.io/vega-lite/docs/config.html#custom-format-type), this value will be passed as `format` alongside `datum.value` to the registered function.
*
* __Default value:__ Derived from [numberFormat](https://vega.github.io/vega-lite/docs/config.html#format) config for number format and from [timeFormat](https://vega.github.io/vega-lite/docs/config.html#format) config for time format.
*/
format?: string | Dict<unknown>;
format?: Format;

/**
* The format type for labels. One of `"number"`, `"time"`, or a [registered custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type).
Expand Down
23 changes: 12 additions & 11 deletions src/compile/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
channelDefType,
DatumDef,
FieldDef,
Format,
isFieldDef,
isFieldOrDatumDefForTimeFormat,
isPositionFieldOrDatumDef,
Expand All @@ -16,7 +17,7 @@ import {fieldValidPredicate} from '../predicate';
import {ScaleType} from '../scale';
import {formatExpression, normalizeTimeUnit, timeUnitSpecifierExpression} from '../timeunit';
import {QUANTITATIVE, Type} from '../type';
import {Dict, stringify} from '../util';
import {stringify} from '../util';
import {isSignalRef} from '../vega.schema';
import {TimeUnit} from './../timeunit';
import {datumDefToExpr} from './mark/encode/valueref';
Expand All @@ -25,7 +26,7 @@ export function isCustomFormatType(formatType: string) {
return formatType && formatType !== 'number' && formatType !== 'time';
}

function customFormatExpr(formatType: string, field: string, format: string | Dict<unknown>) {
function customFormatExpr(formatType: string, field: string, format: Format) {
return `${formatType}(${field}${format ? `, ${stringify(format)}` : ''})`;
}

Expand All @@ -40,7 +41,7 @@ export function formatSignalRef({
config
}: {
fieldOrDatumDef: FieldDef<string> | DatumDef<string>;
format: string | Dict<unknown>;
format: Format;
formatType: string;
expr?: 'datum' | 'parent' | 'datum.datum';
normalizeStack?: boolean;
Expand Down Expand Up @@ -151,7 +152,7 @@ export function formatCustomType({
field
}: {
fieldOrDatumDef: FieldDef<string> | DatumDef<string>;
format: string | Dict<unknown>;
format: Format;
formatType: string;
expr?: 'datum' | 'parent' | 'datum.datum';
normalizeStack?: boolean;
Expand All @@ -176,7 +177,7 @@ export function formatCustomType({
export function guideFormat(
fieldOrDatumDef: FieldDef<string> | DatumDef<string>,
type: Type,
format: string | Dict<unknown>,
format: Format,
formatType: string | SignalRef,
config: Config,
omitTimeFormatConfig: boolean // axis doesn't use config.timeFormat
Expand Down Expand Up @@ -246,7 +247,7 @@ export function numberFormat({
normalizeStack
}: {
type: Type;
specifiedFormat?: string | Dict<unknown>;
specifiedFormat?: Format;
config: Config;
normalizeStack?: boolean;
}) {
Expand Down Expand Up @@ -293,7 +294,7 @@ function formatExpr(field: string, format: string) {
return `format(${field}, "${format || ''}")`;
}

function binNumberFormatExpr(field: string, format: string | Dict<unknown>, formatType: string, config: Config) {
function binNumberFormatExpr(field: string, format: Format, formatType: string, config: Config) {
if (isCustomFormatType(formatType)) {
return customFormatExpr(formatType, field, format);
}
Expand All @@ -304,7 +305,7 @@ function binNumberFormatExpr(field: string, format: string | Dict<unknown>, form
export function binFormatExpression(
startField: string,
endField: string,
format: string | Dict<unknown>,
format: Format,
formatType: string,
config: Config
): string {
Expand All @@ -329,18 +330,18 @@ export function timeFormatExpression({
}: {
field: string;
timeUnit?: TimeUnit;
format?: string | Dict<unknown>;
format?: Format;
formatType?: string;
rawTimeFormat?: string; // should be provided only for actual text and headers, not axis/legend labels
isUTCScale?: boolean;
}): string {
if (!timeUnit || format) {
// If there is no time unit, or if user explicitly specifies format for axis/legend/text.
if (!timeUnit && formatType) {
return `${formatType}(${field}, '${format}')`;
return `${formatType}(${field}, ${stringify(format)})`;
}
format = isString(format) ? format : rawTimeFormat; // only use provided timeFormat if there is no timeUnit.
return `${isUTCScale ? 'utc' : 'time'}Format(${field}, '${format}')`;
return `${isUTCScale ? 'utc' : 'time'}Format(${field}, ${stringify(format)})`;
} else {
return formatExpression(timeUnit, field, isUTCScale);
}
Expand Down

0 comments on commit 0f0558b

Please sign in to comment.