INFLATION - we love it! We hate it. (Hm, we probably just hate it.)
But it seems we love reading about it. Which is not really surprising: inflation and the economy have a significant impact on our livelihoods.
So you might have already heard the good news: itโs coming down! ๐โ ๐ (disclaimer: some places and things!)
But what does reducing inflation mean about prices?
Rather than give you spoilers, hereโs a playground where you can draw inflation and see ๐ the related price effects.
๐จ
Itโs setup with a constant 10% per annum inflation rate, but you can drag that inflation rate handle up or down, and then make it your own scenario by touch or drag to create new handles.
On a desktop, double click or shift-drag to remove handles: fewer handles may be useful to be more deliberate.
Code
md`If you make a mess, you can ${htl.html`<a role="button" href="#" onclick=${(evt) => { viz.data('handle_values', [{ year:0,rate:0.1 },..._.range(1, domainMax).map((year) => ({ year,rate:null }))]).run();update(); evt.preventDefault();}}>Reset</a>`} anyways, or use scenarios provided underneath.`
Code
embed_ =require("vega-embed")
Code
compiled = URL.createObjectURL(newBlob( [`// includes performance tricks// calculations are performed monthly (the input widget lerps between handles)export const month = ({ month_in }) => month_in;// map to an array of monthly ratesexport const month_annual_rate_table_fn = ({ month_annual_rate_table_fn_in }) => month_annual_rate_table_fn_in;export const v = ({ v_in }) => v_in; // interactivity performance trick: memoize by a version number integer instead of table of annual rates; see also custom memo hash belowexport const monthly_rate$ = ({ month_annual_rate_table_fn_in, month_in, v_in }) => { return Math.pow(1 + month_annual_rate_table_fn({ month_annual_rate_table_fn_in })()[month({ month_in })].rate, 1 / 12) - 1; v({ v_in }); // see above, this makes calculang recognise a dependency and recalc values as v changes};export const price$ = ({ month_in, month_annual_rate_table_fn_in, v_in }) => { if (month({ month_in }) == 0) return 1;else return ( price({ month_annual_rate_table_fn_in, v_in, month_in: month({ month_in }) - 1 }) * ( 1 + monthly_rate({ month_annual_rate_table_fn_in, v_in, month_in: month({ month_in }) - 1 })));};// custom memo hash function for better perf/memory tradeoffs vs JSON.stringify default:export const memo_hash$ = ({ month_annual_rate_table_fn_in, ...o }) => Object.values(o);// memoizationexport const monthly_rate$m = memoize(monthly_rate$, memo_hash$);export const monthly_rate = (a) => { return monthly_rate$m(a); // eslint-disable-next-line no-undef monthly_rate$({ month_annual_rate_table_fn_in, month_in, v_in }); // never run, but here to "trick" calculang graph logic};// memoizationexport const price$m = memoize(price$, memo_hash$);export const price = (a) => { return price$m(a); // eslint-disable-next-line no-undef price$({ month_in, month_annual_rate_table_fn_in, v_in }); // never run, but here to "trick" calculang graph logic};// memoizationexport const memo_hash$m = memoize(memo_hash$, memo_hash$);export const memo_hash = (a) => { return memo_hash$m(a); // eslint-disable-next-line no-undef memo_hash$({}); // never run, but here to "trick" calculang graph logic}; // from https://cdn.jsdelivr.net/npm/underscore@1.13.6/underscore-esm.js // Memoize an expensive function by storing its results. function memoize(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!has$1(cache, address)) cache[address] = func.apply(this, arguments); return cache[address]; }; memoize.cache = {}; return memoize; } // Internal function to check whether \`key\` is an own property name of \`obj\`.function has$1(obj, key) { return obj != null && Object.prototype.hasOwnProperty.call(obj, key);}` ], { type:"text/javascript" } ))model =awaitimport(compiled)cartesian = (...a) => a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())))//viewof domainMax = Inputs.select([48 + 2, 12, 13, 14]);domainMax =50dots =cartesian( _.range(0, domainMax -2,1 ) /*[..._.range(0,domainMax,12),..._.range(11,domainMax,12)]*/, [..._.range(0,0.15001,0.01/1),..._.range(-0.01/1,-0.07001,-0.01/1)]).map(([year, rate]) => ({ year, rate }));//mywidth = widthspec = ({//// NOTE: I should put everything in groups, but it happens to be a working structure and some of the overlap is ok/* Some of the names of things: Data sources: - handle_values: keeps a handle value (or null) for each month - handle_values_not_null: just filters above for not null, needed to calculate... - interpolated_values: the interpolated values; populated with rates for every month fed to calculang model to calculate prices - prices: result from calculang model; populated via Vega view API below - dots: this is very important: populated with combo of all choices of month-rates Fed to a voronoi transform; resultant path marks are then where all interactivity is triggered. I didn't try straight rect marks; possibly a good idea (computation for all this should happen only once though) Signals: - set/unset_handle_value triggered by dots selection: maintains handle_values Note: 'year' is used but it should be month or time VEGA-SCENEGRAPH PATCH:: Necessary to ship a hacky change in https://github.com/vega/vega/pull/3547/files for proper interactivity (touch drag) to work on mobile */$schema:"https://vega.github.io/schema/vega/v5.json",autosize:"fit",description:"A basic bar chart example, with value labels shown upon pointer hover.",width: width -30,background:"white",height:450,padding:5,data: [ {name:"handle_values",// init datavalues: [ { year:0,rate:0.1 },..._.range(1, domainMax).map((year) => ({ year,rate:null })) ],on: [ {trigger:"set_handle_value",modify:"data('handle_values')[set_handle_value.year]",// assuming 0 index in yearvalues:"{year:set_handle_value.year, rate:set_handle_value.rate}" }, {trigger:"unset_handle_value",modify:"data('handle_values')[unset_handle_value.year]",// assuming 0 index in yearvalues:"{year:unset_handle_value.year, rate:null}" } ] }, {name:"interpolated_values",// how much slowness does this cause on mobile without calculang? not a lot!source:"handle_values",transform: [ {type:"formula",expr:"if(datum.rate != null, datum.year, null)",as:"year_not_null" }, {type:"filter",expr:"datum.rate != null || 1" }, { type:"window",ops: ["next_value"],fields: ["rate"] }, {type:"window",ops: ["prev_value"],fields: ["rate","year_not_null"] }, { type:"window",ops: ["prev_value"],fields: ["year_not_null"] }, { type:"window",ops: ["next_value"],fields: ["year_not_null"] }, {type:"formula",expr:"if(datum.next_value_rate==null, datum.prev_value_rate, lerp([datum.prev_value_rate,datum.next_value_rate],(datum.year-datum.prev_value_year_not_null)/(datum.next_value_year_not_null-datum.prev_value_year_not_null)))",as:"interpolated" } ] }, {name:"prices",values: _.range(0, domainMax).map((d, i) => ({year:`${d}`,amount:1/** (1+i*0.1)*/ })) }, {name:"handle_values_not_null",source:"handle_values",transform: [{ type:"filter",expr:"datum.rate != null" }] }, {name:"prices_at_handle_values",// effective: note lag year_plus_1source:"handle_values_not_null",transform: [ {type:"formula",as:"year_plus_1",expr:"datum.year+1" }, {type:"lookup",from:"prices",key:"year",as: ["price"],fields: ["year_plus_1"] } ] }, {name:"dots",values: dots,transform: [// x, y a re fields in voronoi, use that ?!?! { type:"formula",as:"x",expr:"scale('xscale', datum.year)" },// voronoi transform requires this ! {type:"formula",as:"y",expr:"scale('handle_yscale', datum.rate)" }, {type:"voronoi",x:"x",y:"y",size: [{ signal:"width" }, { signal:"top_height" }] //extent: [[0,0], [mywidth,150]]//, size: {signal:"width"} } ] } ],/*"signals": [ { "name": "tooltip", "value": {}, "on": [ {"events": "rect:pointerover", "update": "datum"}, {"events": "rect:pointerout", "update": "{}"} ] } ],*/scales: [ {name:"xscale",//"type": "band",domain: { data:"prices",field:"year" },range:"width",padding:30//"round": true }, {name:"handle_yscale",domain: [-0.08,0.16],//.66 here is great - do veronoi and do row facet//"domain": {"data": "prices", "field": "amount"},nice:false,//"range": [0,500]//"height"range: [150,0] } ],axes: [//{ "orient": "bottom", "scale": "xscale", title: 'start of month -', values: [0,12,24,36,48,domainMax-2], labelFontSize: 14, labelFontWeight:'bold', labelColor: 'gray', titleColor: 'gray', grid:true }, {orient:"top",scale:"xscale",title:"month",values: [0,12,24,36,48, domainMax -2],labelFontSize:14,//labelColor: "gray",//titleColor: "gray",grid:true },//{ "orient": "right", "scale": "yscale", grid: true, gridWidth:4, gridColor: 'pink', values:[1,1.25,1.5], titleColor: 'hotpink'/*, title: 'prices'*/, format:'%', labelFontSize: 15, labelColor:'pink', titleFontSize: 20 }, {orient:"left",scale:"handle_yscale",values: [0,0.05,0.1,-0.05,0.15],labelFont:"monospace",labelColor:"steelblue",format:".0%",labelFontSize:16/*, title: 'inflation rate'*/,titleColor:"steelblue",titleFontSize:20,grid:true,labelFontWeight:"bold" } ],signals: [ { name:"top_height",value:150 }, { name:"spacing",value:30 }, {name:"set_handle_value",// add click to remove a handle?on: [ {// cell vs dots//"events": "@cell:click[!event.shiftKey], [@cell:mousedown, window:mouseup] > @cell:mousemove",events:"@cell:click[!event.shiftKey], @cell:touchstart!, @cell:touchmove!{50}, [@cell:pointerdown[!event.shiftKey], window:pointerup] > @cell:pointermove!",//"events": "@cell:touchmove",update:"datum" } ] },/* {name: "create"x_handle_debounced", on: [{ events }]}*/ {name:"unset_handle_value",// add click to remove a handle?on: [ {// cell vs dots// see triggers in https://vega.github.io/vega/docs/transforms/voronoi/events:"@cell:click[event.shiftKey], @cell:dblclick, [@cell:pointerdown[event.shiftKey], window:pointerup] > @cell:pointermove",update:"datum" } ] } ],marks: [//,/*{ "type": "symbol", name: 'dots', "from": {"data":"dots"}, "encode": { "enter": { shape: {value: 'square'}, //x: {value: 8}, "x": {"scale": "xscale", field: 'year'}, "y": {"scale": "handle_yscale", "field": "rate"}, "fill": {value: 'red'},//{"signal": "datum.rate > 0 ? 'lightgreen' : datum.rate == 0 ? 'lightblue' : 'pink'"}, size: {value: 400}, }, "update": { //"opacity": {"signal": "if(datum.actual_handle_rate==null, 0, if(abs(datum.actual_handle_rate - datum.rate)<0.05,0.4,0) )"}, }, } },*//* { "type": "text", "encode": { "enter": { "align": {"value": "center"}, "baseline": {"value": "bottom"}, "fill": {"value": "#333"} }, "update": { "x": {"scale": "xscale", "signal": "tooltip.year", "band": 0.5}, "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2}, "text": {"signal": "tooltip.amount"}, "fillOpacity": [ {"test": "datum === tooltip", "value": 0}, {"value": 1} ] } } }*/ {//"type": "path", //type:"symbol",interactive:false,from: { data:"handle_values_not_null" },encode: {enter: {shape: { value:"circle" },//path: {value: 'M -3 0 L -3 -6 L -9 -6 L 0 -18 L 9 -6 L 3 -6 L 3 6 L 9 6 L 0 15 L -9 6 L -3 6 Z'},//x: {value: 8},stroke: { value:"black" },x: { scale:"xscale",field:"year" },size: { value:100 } },update: {y: { scale:"handle_yscale",field:"rate" },fill: {signal:"datum.rate > 0 ? 'lightgreen' : datum.rate == 0 ? 'lightblue' : 'pink'" } }/*"hover": { // better to overlay actual handles. Voronoi in the way ?? size: {value: 2800}, }*/ } }, {type:"line",from: { data:"interpolated_values" },interactive:false,encode: {enter: {//strokeCap: {value: 'butt'}, // rendering bugs//strokeCap: {value: 'round'},//strokeDash: {value: [1,6]},interpolate: { value:"step-after" },stroke: { value:"red" },strokeWidth: { value:3 },x: { scale:"xscale",signal:"datum.year-0.5" },size: { value:70 },opacity: { value:0.8 } },update: {y: { scale:"handle_yscale",field:"interpolated" } } } },// NOT OPTIMISED {type:"group",encode: {enter: {x: { value:0 },y: { value:0 },width: { signal:"width" },// should be updateheight: { signal:"top_height" },stroke: { value:"steelblue" },strokeWidth: { value:"4" } } },marks: [ {type:"text",interactive:false,encode: {enter: {x: { value:10 },y: { value:5 },baseline: { value:"top" },text: { value:"inflation rate p.a." },fill: { value:"steelblue" },opacity: { value:0.8 },fontSize: { value:20 },fontWeight: { value:"bold" },fontStyle: { value:"italic" } } } }, {interactive:false,type:"rect",encode: {enter: {x: { value:0 },y: { scale:"handle_yscale",value:0 },width: { signal:"width" },y2: { scale:"handle_yscale",value:-0.08 },fill: { value:"grey" },opacity: { value:0.1 } } } }, {type:"path",name:"cell",from: { data:"dots" },encode: {enter: {fill: { value:"transparent" },opacity: { value:0.01 },path: { field:"path" },stroke: { value:"blue" } } } } ] }, {type:"group",axes: [ {orient:"right",scale:"yscale",grid:true,//gridWidth: 1,//gridColor: "pink",values: [1,1.2,1.5,0.8,0.5],titleColor:"pink"/*, title: 'prices'*/,format:"%",labelFontSize:17,labelColor:"purple",labelFontWeight:"bold",labelFont:"monospace",titleFontSize:20,strokeWidth:10,encode: {ticks: {enter: { stroke:null } },domain: {enter: { stroke:null } } } }, {orient:"bottom",scale:"xscale",encode: {domain: {enter: { stroke:null } },ticks: {enter: { stroke:null } } },//title: "start of month -",values: [0,12,24,36,48, domainMax -2],labelFontSize:10//labelFontWeight: "bold"//labelColor: "gray",//titleColor: "gray",//grid: true } ],scales: [ {name:"yscale",domain: [0.75,1.5],//domain: { data: "prices", field: "amount" },zero:false,//"nice": true,//"range": "height"range: [{ signal:"height - top_height - spacing" },0] } ],encode: {enter: {x: { value:0 },y: { signal:"top_height + spacing" },width: { signal:"width" },// these should be updateheight: { signal:"height-top_height-spacing" }//stroke: { value: "orange" },//strokeWidth: { value: "1" } } },marks: [ {type:"text",encode: {enter: {x: { signal:"width/2" },// move to updatey: { value:10 },text: { value:"=> price" },fill: { value:"purple" },fontSize: { value:30 },fontWeight: { value:"bold" },fontStyle: { value:"italic" },//font: { value: "monospace" }align: { value:"right" } } } }, {//"type": "path", //type:"symbol",interactive:false,from: { data:"prices_at_handle_values" },encode: {enter: {shape: { value:"square" },//path: {value: 'M -3 0 L -3 -6 L -9 -6 L 0 -18 L 9 -6 L 3 -6 L 3 6 L 9 6 L 0 15 L -9 6 L -3 6 Z'},//x: {value: 8},//stroke: { value: "black" },x: { scale:"xscale",field:"year_plus_1" },size: { value:80 },opacity: { value:0.6 } },update: {y: { scale:"yscale",field:"price.amount" },fill: {signal:"datum.rate > 0 ? 'lightgreen' : datum.rate == 0 ? 'lightblue' : 'pink'" } }/*"hover": { // better to overlay actual handles. Voronoi in the way ?? size: {value: 2800}, }*/ } }, {type:"line",//tooltip: true,interactive:false,from: { data:"prices" },encode: {enter: {/*tooltip: { signal: "{'Price': format(datum.amount, '0.1%'), 'start of month': datum.year}" },*///strokeCap: {value: 'butt'}, // rendering bugs//strokeCap: {value: 'round'},//strokeDash: {value: [1,6]},//interpolate: { value: "step-after" },stroke: { value:"purple" },strokeWidth: { value:4 },// 6//dy: { value: -30 },x: { scale:"xscale",signal:"datum.year-0.5" },// use band instead?size: { value:70 },y: { scale:"yscale",field:"amount" } // } } },, ] } ]})viewof viz =embed_(spec, { renderer:"canvas",actions:false });mutable v_in =0;mutable rates = viz.data("interpolated_values");functionupdate() { mutable v_in++; model.price$m.cache= {}; model.monthly_rate$m.cache= {}; mutable rates = viz.data("interpolated_values");};a = viz.addSignalListener("set_handle_value", (_) => {update();});b = viz.addSignalListener("unset_handle_value", (_) => {update();});// known BUG: interactions lost on screen rotation change// this messes visibility of scales? even with .resize()//viz.signal('width', width-80).run()zz = viz // give a name just to mitigate output.data("prices", _.range(0, domainMax).map((d, i) => ({year:`${d}`,amount: model.price({ v_in,month_in: d,month_annual_rate_table_fn_in: () => rates.map((d) => ({ year: d.year,rate: d.interpolated })) }) })) )//.resize().run();
Inflation rates are annual rates which are converted for calculations carried out at monthly intervals. Linear interpolation is applied between handles, first. Interpolated values are indicated by steps in the red line.
Price values are expressed relative to a base: 100% at start of month 0. The price y scale begins at 75%: not 0% (so itโs misleading to interpret using height relativities alone: you must refer to the y scale)
A note to teachers ๐งโ๐ซ
If you wish to use and modify this interaction in a lesson about inflation, then you can.
If you wish to use it in a different lesson (like calculus), then you can also talk to me to make it better for you.
Playground vs. reality
In the real-world, inflation figures are determined from price observations.
In this playground, itโs the other way around!
Iโm not an economist, but I suppose an economist might clarify about the real-world that inflation figures donโt determine future prices. (But they may have some effect on expectations)
A challenge!
If youโre up for a challenge then hereโs a hard one:
In the fall of 1972 President Nixon announced that the rate of increase of inflation was decreasing. This was the first time a sitting president used the third derivative to advance his case for reelection.