Skip to content

Instantly share code, notes, and snippets.

@Giammaria
Last active January 26, 2025 21:06
Show Gist options
  • Save Giammaria/7d8cb27a0ecc9f9a6afb3625cf4f1fcc to your computer and use it in GitHub Desktop.
Save Giammaria/7d8cb27a0ecc9f9a6afb3625cf4f1fcc to your computer and use it in GitHub Desktop.
20250120_dynamic_date_granularity
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 800,
"height": 300,
"autosize": "fit-y",
"padding": 5,
"signals": [
{"name": "ganttWidth", "update": "width"},
{
"name": "dateGranularityPre",
"init": "0.5",
"bind": {
"name": "Date Granularity",
"input": "range",
"min": 0,
"max": 1,
"step": 0.005
}
},
{
"name": "horizontalScrollPercentage",
"init": "0.5",
"bind": {
"name": "Horizontal Scroll",
"input": "range",
"min": 0,
"max": 1,
"step": 0.01
}
},
{
"name": "minDateBandwidths",
"init": "{hour: 20, day: 20, month: 20, year: 20}"
},
{
"name": "minDateUnit",
"init": "'hours'",
"bind": {
"name": "Min Date Unit",
"input": "select",
"options": ["hours", "days", "months", "years"]
}
},
{"name": "today", "update": "utc(year(now()),month(now()),date(now()))"},
{
"name": "hourBandwidth",
"update": "scale('x', timeOffset('hours', utchours(0,0,0,0),1)) - scale('x', utchours(0,0,0,0))"
},
{"name": "currentXBandwidth", "update": "(round(hourBandwidth *100)/100)"},
{
"name": "thresholdMinuteBandwidth",
"update": "17.78*(span(currentXDateProperties.domain)/63162000000)"
},
{
"name": "thresholdHourBandwidth",
"update": "0.6*(span(currentXDateProperties.domain)/63162000000)"
},
{
"name": "thresholdDayBandwidth",
"update": "0.26*(span(currentXDateProperties.domain)/63162000000)"
},
{
"name": "thresholdMonthBandwidth",
"update": "0.04*(span(currentXDateProperties.domain)/63162000000)"
},
{
"name": "thresholdYearBandwidth",
"update": "0.03*(span(currentXDateProperties.domain)/63162000000)"
},
{
"name": "xCurrentUnit",
"update": "(currentXBandwidth>=thresholdMinuteBandwidth ? 'hour': currentXBandwidth>=thresholdHourBandwidth ? 'day' : currentXBandwidth >= thresholdDayBandwidth ? 'month' : currentXBandwidth >= thresholdMonthBandwidth ? 'month' : 'year')"
},
{
"name": "xCurrentDateFormat",
"update": "['%H', '%d', currentXBandwidth >= 0.12 ? '%B %Y' : '%b', '%Y'][indexof(['hour', 'day', 'month', 'year'],xCurrentUnit)]"
},
{
"name": "xCurrentDateLabelDX",
"update": "xCurrentUnit === 'hour' ? ((scale('x', utchours(0,0,0,0))-scale('x', timeOffset('hours', utchours(0,0,0,0), -1)))/2) : (scale('x', now())-scale('x', timeOffset(xCurrentUnit, now(), -1)))/2"
},
{
"name": "xCurrentUnitOffset",
"update": "xCurrentUnit === 'year' ? null : ['hour', 'day', 'month', 'year'][indexof(['hour','day', 'month'], xCurrentUnit)+1]"
},
{
"name": "xCurrentDateFormatOffset",
"update": "['%H', '%d %B %Y', '%B %Y', '%Y'][indexof(['hour', 'day', 'month', 'year'],xCurrentUnitOffset)]"
},
{
"name": "xCurrentDateOffsetLabelDX",
"update": "xCurrentUnitOffset === 'hour' ? ((scale('x', utchours(0,0,0,0))-scale('x', timeOffset('hours', utchours(0,0,0,0), -1)))/2) : (scale('x', now())-scale('x', timeOffset(xCurrentUnitOffset, now(), -1)))/2"
},
{
"name": "minDatebandwidth",
"update": "[3.6e+6, 8.64e+7, 2.628e+9, 3.154e+10][indexof(['hours', 'days', 'months', 'years'], minDateUnit)]"
},
{
"name": "hourDomain",
"update": "[data('xDomain')[0]['minDate']-scale('dateUnitIncrementMS', 'hours'), data('xDomain')[0]['minDate'] + ((ganttWidth-minDateBandwidths.hour)/minDateBandwidths.hour)*scale('dateUnitIncrementMS', 'hours')]"
},
{
"name": "currentXDateProperties",
"update": "{domain: data('xDomain')[0].domain, span: span(data('xDomain')[0].domain)}"
},
{"name": "columnsWidth", "value": 0},
{
"name": "dateGranularity",
"init": "{value: 0.5, zoomDelta: 0}",
"on": [
{
"events": {"signal": "dateGranularityPre"},
"update": "{value: dateGranularityPre, zoomDelta: dateGranularityPre > dateGranularity.value ? pow(1.01, abs(dateGranularityPre - dateGranularity.value) * 1000) : 1 / pow(1.01, abs(dateGranularity.value - dateGranularityPre) * 1000)}"
}
]
},
{
"name": "zoom",
"value": 1,
"on": [
{
"events": "wheel!",
"force": true,
"update": "x()>columnsWidth?pow(1.001, (event.deltaY) * pow(16, event.deltaMode)):1"
},
{
"events": {"signal": "dateGranularity"},
"update": "dateGranularity.zoomDelta"
}
]
},
{
"name": "xDomPre",
"value": [0, 0],
"on": [
{
"events": {"signal": "zoom"},
"update": "[initialDomainCenter + (xDom[0] - initialDomainCenter) * zoom, initialDomainCenter + (xDom[1] - initialDomainCenter) * zoom]"
},
{
"events": {"signal": "horizontalScrollPercentage"},
"update": "[initialDomainCenter - span(xDom) / 2 + span(currentXDateProperties.domain) * horizontalScrollPercentage, initialDomainCenter + span(xDom) / 2 + span(currentXDateProperties.domain) * horizontalScrollPercentage]"
}
]
},
{
"name": "initialDomainCenter",
"init": "(data('xDomain')[0]['minDate'] + data('xDomain')[0]['maxDate']) / 2"
},
{"name": "anchorDate", "init": "initialDomainCenter"},
{"name": "xDomMinSpan", "update": "span(data('xDomain')[0]['hourDomain'])"},
{
"name": "xDomMaxSpan",
"update": "round((ganttWidth/0.15)* scale('dateUnitIncrementMS', 'years'))"
},
{
"name": "xDom",
"update": "currentXDateProperties.domain",
"on": [
{
"events": {"signal": "xDomPre"},
"update": "span(xDomPre)<xDomMinSpan ? [initialDomainCenter - xDomMinSpan/2, initialDomainCenter + xDomMinSpan/2] : span(xDomPre)>xDomMaxSpan ? [initialDomainCenter - xDomMaxSpan/2, initialDomainCenter + xDomMaxSpan/2] : xDomPre"
}
]
}
],
"marks": [
{
"type": "text",
"encode": {
"update": {
"x": {"value": 20},
"y": {"value": 20},
"text": {
"signal": "[utcFormat(xDom[0], '%d-%b-%Y %H:%M:%S.%LZ'), utcFormat(xDom[1], '%d-%b-%Y %H:%M:%S.%LZ')]"
},
"fill": {"value": "black"}
}
}
},
{
"type": "text",
"encode": {
"update": {
"x": {"value": 20},
"y": {"value": 60},
"text": {
"signal": "['currentXBandwidth: ' + currentXBandwidth, '‎', 'thresholdMinuteBandwidth: ' + thresholdMinuteBandwidth, 'thresholdHourBandwidth: ' + thresholdHourBandwidth, 'thresholdDayBandwidth: ' + thresholdDayBandwidth, 'thresholdMonthBandwidth: '+thresholdMonthBandwidth, 'thresholdYearBandwidth: ' + thresholdYearBandwidth, '‎']"
},
"fill": {"value": "black"}
}
}
},
{
"type": "text",
"encode": {
"update": {
"x": {"value": 20},
"y": {"value": 160},
"text": {
"signal": "['xCurrentUnit: ' + xCurrentUnit, 'xCurrentDateFormat: ' + xCurrentDateFormat, 'xCurrentDateLabelDX: '+xCurrentDateLabelDX]"
},
"fill": {"value": "black"}
}
}
},
{
"type": "text",
"encode": {
"update": {
"x": {"value": 20},
"y": {"value": 210},
"text": {
"signal": "['xCurrentUnitOffset: ' + xCurrentUnitOffset, 'xCurrentDateFormatOffset: ' + xCurrentDateFormatOffset, 'xCurrentDateOffsetLabelDX: '+xCurrentDateOffsetLabelDX]"
},
"fill": {"value": "black"}
}
}
}
],
"scales": [
{
"name": "dateUnitIncrementMS",
"type": "band",
"domain": ["hours", "days", "months", "years"],
"range": [3600000, 86400000, 2628000000, 31540000000]
},
{
"name": "x",
"type": "time",
"domain": {"signal": "xDom"},
"range": {"signal": "[0,ganttWidth]"}
}
],
"axes": [
{
"description": "Bottom date axis",
"ticks": true,
"labelPadding": {"signal": "xCurrentDateFormat === '%B %Y' ? 2 :-12"},
"scale": "x",
"position": {"signal": "columnsWidth"},
"orient": "top",
"domainColor": "#CCC",
"domainWidth": 1,
"tickSize": 15,
"tickOpacity": 1,
"grid": true,
"zindex": -1,
"labelOverlap": true,
"formatType": "time",
"labelBound": true,
"tickCount": {
"signal": "xCurrentUnit === 'hour' ? 24*(span(xDom)/86400000) : xCurrentUnit"
},
"format": {"signal": "xCurrentDateFormat"},
"labelSeparation": 3,
"tickExtra": true,
"encode": {
"labels": {
"update": {
"dx": {"signal": "xCurrentDateLabelDX"},
"text": {"signal": "xCurrentDateFormat === '%B %Y' ? split(datum.label, ' ') : datum.label"}
}
},
"ticks": {
"update": {
"stroke": {"value": "#CCC"},
"strokeWidth": {
"signal": "xCurrentUnit === 'day' && datum.label === '01' ? 0 : indexof(datum.label, 'Jan') >= 0 ? 0 : 1"
}
}
},
"grid": {
"update": {
"stroke": {"value": "#CCC"},
"strokeWidth": {
"signal": "xCurrentUnit === 'day' && datum.label === '01' ? 0 : indexof(datum.label, 'Jan') >= 0 ? 0 : 1"
}
}
}
}
},
{
"description": "Top date axis",
"title": {
"signal": "xCurrentDateFormatOffset === '%B %Y' && (toDate(utcFormat((xDom[0] + (xDom[1]-xDom[0])/2), '01-%B-%Y')) < domain('x')[0] && toDate(utcFormat((xDom[0] + (xDom[1]-xDom[0])/2), '15-%B-%Y')) > domain('x')[1]) ? utcFormat((xDom[0] + (xDom[1]-xDom[0])/2), '%B %Y') : null"
},
"titleX": {"signal": "columnsWidth+ganttWidth/2"},
"titleY": -25,
"titleBaseline": {"value": "middle"},
"titleFontSize": {"value": 10},
"titleFontWeight": "400",
"scale": "x",
"position": {"signal": "columnsWidth"},
"domain": false,
"orient": "top",
"offset": 0,
"tickSize": 20,
"tickOpacity": 1,
"tickWidth": 1,
"tickColor": "#666",
"labelBaseline": "middle",
"grid": true,
"gridWidth": 1,
"gridColor": {"value": "#666"},
"zindex": 1,
"labelOverlap": true,
"formatType": "time",
"labelBound": true,
"tickCount": {"signal": "(xCurrentUnitOffset || 0)"},
"format": {"signal": "xCurrentDateFormatOffset"},
"labelSeparation": 5,
"encode": {
"labels": {
"update": {
"text": {
"signal": "xCurrentDateFormat === '%B %Y' ? null : datum.label"
},
"x": {
"signal": "xCurrentDateFormatOffset === '%Y' ? toDate(utcFormat(datum.value, '01-Jun-%Y')) > xDom[1] ? scale('x', datum.value)+(scale('x', xDom[1])-scale('x', datum.value))/2 : scale('x', toDate(utcFormat(datum.value, '01-Jun-%Y'))) : xCurrentDateFormatOffset === '%B %Y' ? toDate(utcFormat(datum.value, '17-%b-%Y')) > xDom[1] ? scale('x', datum.value)+(scale('x', xDom[1])-scale('x', datum.value))/2 : scale('x', toDate(utcFormat(datum.value, '15-%b-%Y'))) : scale('x', datum.value)"
},
"dx": {
"signal": "xCurrentDateFormatOffset === '%Y' || xCurrentDateFormatOffset === '%B %Y' ? 0 : xCurrentDateOffsetLabelDX"
},
"align": {"value": "center"}
}
}
}
}
],
"data": [
{
"name": "dataset",
"values": [{"startDate": "2020-01-01"}],
"transform": [
{
"type": "formula",
"expr": "minDatebandwidth",
"as": "minDatebandwidth"
},
{"type": "aggregate", "groupby": ["startDate", "minDatebandwidth"]},
{
"type": "formula",
"expr": "sequence(toDate(datum.startDate), timeOffset('year', toDate(datum.startDate), 2), datum.minDatebandwidth)",
"as": "startDate"
},
{"type": "flatten", "fields": ["startDate"]},
{"type": "formula", "expr": "datum.startDate", "as": "endDate"},
{
"type": "formula",
"expr": "utcFormat(datum.startDate, '%Y-%m-%dT%H:%M:%S.%LZ')",
"as": "startDateFormatted"
},
{
"type": "formula",
"expr": "utcFormat(datum.endDate, '%Y-%m-%dT%H:%M:%S.%LZ')",
"as": "endDateFormatted"
},
{
"type": "project",
"fields": [
"startDate",
"endDate",
"startDateFormatted",
"endDateFormatted",
"minDatebandwidth"
]
}
]
},
{"name": "dates", "source": "dataset", "transform": []},
{
"name": "xDomain",
"source": "dates",
"transform": [
{
"type": "aggregate",
"fields": ["startDate", "endDate"],
"ops": ["min", "max"],
"as": ["minDate", "maxDate"]
},
{
"type": "formula",
"expr": "(datum.maxDate-datum.minDate)/scale('dateUnitIncrementMS', 'hours')",
"as": "hours"
},
{
"type": "formula",
"expr": "[datum.minDate-scale('dateUnitIncrementMS', 'hours'), datum.minDate + ((ganttWidth-minDateBandwidths.hour)/minDateBandwidths.hour)*scale('dateUnitIncrementMS', 'hours')]",
"as": "hourDomain"
},
{
"type": "formula",
"expr": "[datum.minDate - scale('dateUnitIncrementMS', 'hours'), datum.minDate + ganttWidth*0.7*scale('dateUnitIncrementMS', 'hours')]",
"as": "dayDomain"
},
{
"type": "formula",
"expr": "[datum.minDate - scale('dateUnitIncrementMS', 'hours'), datum.minDate + ganttWidth*0.5*scale('dateUnitIncrementMS', 'hours')]",
"as": "monthDomain"
},
{
"type": "formula",
"expr": "[datum.minDate - scale('dateUnitIncrementMS', 'hours'), datum.minDate + ganttWidth*0.3*scale('dateUnitIncrementMS', 'hours')]",
"as": "yearDomain"
},
{
"type": "formula",
"expr": "[datum.minDate-scale('dateUnitIncrementMS', 'hours'),datum.maxDate+scale('dateUnitIncrementMS', 'hours')]",
"as": "allDomain"
},
{"type": "formula", "expr": "datum.allDomain", "as": "domain"},
{
"type": "formula",
"expr": "[utcFormat(datum.domain[0], '%d-%b-%Y %H:%M:%S.%LZ'), utcFormat(datum.domain[1], '%d-%b-%Y %H:%M:%S.%LZ')]",
"as": "domainFormatted"
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment