Public
Edited
Sep 21, 2024
Importers
Insert cell
Insert cell
Insert cell
Insert cell
md`## Utility Functions`
Insert cell
xAxis = (x, g) =>
g.attr("transform", `translate(0,${height - margin.bottom})`).call(
d3
.axisBottom(x)
.ticks(5)
.tickSizeOuter(10)
)
Insert cell
yAxis = (data, y, g) =>
g
.attr("transform", `translate(${chartWidth - margin.right},0)`)
.call(
d3
.axisRight(y)
.ticks(5)
.tickSize(15)
)
.call(g => g.select(".domain").remove())
.call(g =>
g
.selectAll(".tick text")
.attr("transform", `translate(${-15},-5)`)
.attr('text-anchor', 'center')
)
.call(g =>
g
.selectAll(".tick line")

.attr('transform', `translate(${15 / 2},0)`)
.attr('stroke', 'black')
.attr('x1', function() {
return +d3.select(this).data() === 0
? -chartWidth + margin.right
: -15;
})
.attr('stroke', 'silver')
)

.call(g =>
g
.select(".tick:first-of-type line")
.clone()
.attr("x", 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text('value')
)
Insert cell
callout = (g, indicator, value, date) => {
if (!value) return g.style("display", "none");

g.style("display", null)
.style("pointer-events", "none")
.style("font", "10px sans-serif");

const path = g
.selectAll("path")
.data([null])
.join("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr('opacity', 0.7)

let data = ["tooltip-value", "tooltip-name", "tooltip-date"];
const text = g
.selectAll("text")
.data([null])
.join("text")
.call(text =>
text
.selectAll("tspan")
.data([value, indicator, date])
.join("tspan")
.attr('class', (d, i) => data[i])
.attr("x", 0)
.attr("y", (d, i) => `${(i + 1) * 1.5}rem`)
.attr('font-size', (d, i) => {
return i === 0 ? '1.5rem' : '0.8rem';
})
.style("font-weight", (_, i) => (i ? null : "bold"))
.text(d => d)
);

const { x, y, width: w, height: h } = svgBBox(text.node()); //text.node().getBBox();
// mutable tooltip =
text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr(
"d",
`M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`
);
}
Insert cell
chartWidth = Math.max(400, width / 2)
Insert cell
GenerateLineChart = (indicator) => {
let data = [...indicator.history["PX_LAST"]].map((x) => {
let obj = {};
obj.date = d3.timeParse("%Y-%m-%d")(x.date);
obj.value = +x.value;
return obj;
});
let indicatorName = MetaInfo.filter(
(x) => indicator.ticker === x.tickerID + " Index"
)[0];

let svgRef = DOM.svg(chartWidth, height);
let svg = d3
.select(svgRef)
// .style('border', '6px double ' + d3.lab(46, 74, 83))
// .attr('viewBox', `${0},${0},${chartWidth},${height}`)
.attr("id", DOM.uid(indicator.ticker.split(" ")[0]));

let xScale = d3
.scaleUtc()
.domain(d3.extent(data, (d) => d.date))
.range([margin.left, chartWidth - margin.right]);
let yScale = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.value))
.nice()
.range([height - margin.bottom, margin.top]);
let line = d3
.line()
.curve(d3[curveSelection])
.defined((d) => !isNaN(d.value))
.x((d) => xScale(d.date))
.y((d) => yScale(d.value));

svg.append("g").call((g) => xAxis(xScale, g));
svg.append("g").call((g) => yAxis(data, yScale, g));

const path = svg
.append("g")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.append("path")
.datum(data)
.join("path")
.style("mix-blend-mode", "multiply")
.attr("d", line);
const tooltip = svg.append("g");

function bisect(mx) {
const bisect = d3.bisector((d) => d.date).left;

const date = xScale.invert(mx);
const index = bisect(data, date, 1);
const a = data[index - 1];
const b = data[index];
return date - a.date > b.date - date ? b : a;
}

svg.on("touchmove mousemove", function () {
const { date, value } = bisect(d3.mouse(this)[0]);

tooltip
.attr("transform", `translate(${xScale(date)},${yScale(value)})`)
.call(
callout,
indicatorName.heading,
value,
d3.timeFormat("%b. %Y")(date)
);
});

svg.on("touchend mouseleave", () => tooltip.call(callout, null));
return svgRef;
}
Insert cell
GenerateInfo = data => {
let indicator = data.ticker;
let recentVal = d3.greatest(data.history.PX_LAST, d => d.date);
let indexUnits, quoteUnits;
let symbol = '';
let direction = "";
let before,
after = "";

if (data.reference.INDX_UNITS === 'Value') {
after =
data.reference.QUOTE_UNITS === undefined
? ''
: data.reference.QUOTE_UNITS.toLowerCase();
symbol = 'zero';

if (+recentVal.value > 1000) {
after = 'billions';
}
} else if (data.reference.INDX_UNITS === 'Percent') {
after = data.reference.QUOTE_UNITS === 'Points' ? 'points' : 'pct';
} else if (data.reference.INDX_UNITS === 'Rate') {
after = data.reference.QUOTE_UNITS === '% CHANGE' ? 'pct_change' : 'pct';
if (after === 'pct_change')
direction = +recentVal.value > 0 ? 'up' : 'down';
}
// let isPoint =

let lastDate = data.reference.ECO_FUTURE_RELEASE_DATE_LIST.map(
x => new Date(x['Release Dates & Times'])
)
.sort((a, b) => b - a)
.find(d => d < new Date());
let infoObj = MetaInfo.filter(x => indicator === x.tickerID + ' Index')[0];
return html`

<div class="text ${indicator} info">
<div class="last-value ${before} ${after} ${direction} ${symbol}">${
recentVal.value
}</div>
<h2 class="indicator-heading">${infoObj.heading}</h2>

<div class="indicator-description">
${infoObj.description}<span class="periodicity"> (${
data.reference.INDX_FREQ
})</span>

<div class="dates">
<span class="last ${indicator}"><sub>Last: </sub> ${lastNextDTFormatter(
lastDate
)}</span>
<span class="next ${indicator}"><sup>Next: </sup> ${lastNextDTFormatter(
new Date(data.reference.ECO_FUTURE_RELEASE_DATE)
)}</span>
</div>
</div>
</div>
`;
}
Insert cell
lastNextDTFormatter = d3.timeFormat('%b %d, %Y')
Insert cell
//////////////////////SHAPE OF DATA///////////////
/*
ticker: "PCE CRCH Index"
history:

Object {PX_LAST: Array(141)}
reference:

Object {
INDX_UNITS: "Rate"
SOURCE_NAME: "Bureau of Economic Analysis"
INDX_FREQ: "Monthly"
ECO_FUTURE_RELEASE_DATE_LIST:

Array(36) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
ECO_FUTURE_RELEASE_DATE: "2019/11/27 10:00:00"
PX_DISP_FORMAT_MAX_NUM_DEC: 1
REGION_OR_COUNTRY: "United States"
QUOTE_UNITS: "% CHANGE"
}
lastReleaseDate: "2019-10-31T12:30:00Z"
*/
Insert cell
md`---
### Data Exploration and Wrangling (Rough-Work/Play)

`
Insert cell
d3.groups(
FileData.data,
d => d.reference.INDX_UNITS,
d => d.reference.QUOTE_UNITS
)
Insert cell
md`---
## Different kinds of units`
Insert cell
d3.groups(FileData.data, d => d.reference.QUOTE_UNITS)
Insert cell
md`---
## Regions`
Insert cell
d3.groups(FileData.data, d => d.reference.REGION_OR_COUNTRY)
Insert cell
Euro[0]
Insert cell
US[1]
Insert cell
Info = [
[
"49.4",
"MPMIGLMA",
"IHS Markit and JPMorgan Chase’s snapshot of the health of manufacturing around the world, based on surveys of multiple purchasing managers on their activity. A number above 50 signals expansion.",
"Updates monthly Last July 1, 2019",
""
],
[
"+224K",
"NFP TCH",
"A measure of how tight the labor market is running in the world’s biggest economy.",
"Updates monthly Last July 5, 2019",
""
],
[
"+0.4%",
"PCE CRCH",
"The U.S. consumer is typically one of the pillars of the world economy and this shows how they are faring.",
"Updates monthly Last June 28, 2019",
""
],
[
"+1.5%",
"PCE DEFY",
"The Federal Reserve’s preferred measure of inflation.",
"Updates monthly Last June 28, 2019",
""
],
[
"+0.46%",
"BZGDYOY%",
"A proxy for commodity exporters throughout the world.",
"Updates quarterly Last May 30, 2019",
""
],
[
"$2.63B",
"CMINCOPR",
"Chile’s copper output. From autos to TV sets to high-tech wiring everywhere needs copper and the Andean nation is the world’s largest producer. If demand is strong, the world economy is likely to be too.",
"Updates monthly Last July 8, 2019",
""
],
[
"+1.3%",
"ECCPEMUY",
"A gauge of inflation in the euro area and the main measure watched by the European Central Bank.",
"Updates monthly Last July 17, 2019",
""
],
[
"97.5",
"GRIFPBUS",
"The leading indicator of health in the euro area’s lynchpin economy, it is based on a survery of about 7,000 executives in German manufacturing, services, retail, wholesale and construction companies. The aim is to guage their assessment of how the economy stands and their outlook for it.",
"Updates monthly Last July 25, 2019",
""
],
[
"+49.4%",
"CPMINDX",
"China is the largest manufacturer of autos, smartphones and other goods the world over so this index provides a key insight into the heart of global production.",
"Updates monthly Last June 29, 2019",
""
],
[
"0.0%",
"CHEFTYOY",
"How much companies are charging at the factory gate in China provides a glimpse into global inflation trends.",
"Updates monthly Last July 9, 2019",
""
],
[
"+0.6%",
"JNCPIXFF",
"A measure of price pressures and test of how long the world’s most audacious monetary experiment continues.",
"Updates monthly Last July 18, 2019",
""
],
[
"−13.5%",
"KOEXTOTY",
"An insight into demand for one of Asia’s key exporters especially of technology.",
"Updates monthly Last June 30, 2019",
""
]
]
Insert cell
Heading = [
"Global",
"Global PMI",
"U.S.",
"U.S. employment",
"U.S. consumer spending",
"U.S. personal consumption expenditures",
"Latin America",
"Brazil GDP",
"Chile copper exports",
"Europe",
"Euro-area inflation",
"German Ifo",
"Asia",
"China manufacturing PMI",
"China PPI",
"Japan inflation",
"South Korea exports"
]
Insert cell
region_headings = d3.permute(Heading, [0, 2, 6, 9, 12])
Insert cell
indicator_headings = Heading.filter(x => !region_headings.includes(x))
Insert cell
MetaInfo = d3.zip(Info, indicator_headings).map(x => {
let obj = {};
obj.heading = x[1];
obj.tickerID = x[0][1];
obj.change = x[0][0];
obj.description = x[0][2];
obj.lastUpdate = x[0][3];
return obj;
})
Insert cell
getPerformance = data =>
d3.pairs(data, ({ value: previous }, { date, value }) => {
return { date, value: (+close - +previous) / +previous };
})
Insert cell
{
let data = Euro[0].history.PX_LAST;
let latestData = data[data.length - 1].value;
let previousData = data[data.length - 2].value;
return previousData - latestData / previousData;
}
Insert cell
Arrow = direction => {
return (
d3
.select(DOM.svg(50, 50))
.attr('width', width / 3)
.attr('height', width / 6)
.attr('preserveAspectRatio', "xMinYMax slice")

.attr('viewBox', [0, 0, 50, 25])

// .style('border', '1px black solid')
.call(g => {
g.append('path')
.attr('d', function() {
return direction === '+'
? 'M25 0L50 25L50 25L0 25Z'
: 'M0 25L25 0L50 24L0 25Z';
})
// .attr('stroke', 'black')
.attr('stroke-width', 1)
// .attr('stroke-miterlimit', '3')

.attr('fill', () => (direction === '+' ? '#00C88A' : '#FF415F'));
})
.node()
);
}
Insert cell
GenerateHorizontalDivider = () => {
return html`<svg height=20 width=${width}><path stroke="black" fill="#f5f5f5" stroke-width=0.5 d="M0 10${d3
.range(10, width, 2)
.map((x, i) => {
let y = i % 2 === 0 ? 5 : 15;
return "L" + x + " " + y;
})
.join("")}V 10L0 10"></path></svg>`;
}
Insert cell
md`## DATA`
Insert cell
md`<i>data source: [bloomberg](https://www.bloomberg.com/bbg-gfx/graphics-data/economic-indicators/master/data.json)</i>`
Insert cell
RegionalData = ({
Global: [Global],
Asia: Asia,
'U.S.A.': US,
'Latin America': SouthAmerica,
'Euro Zone': Euro
})
Insert cell
Global = FileData.data[0]
Insert cell
Asia =[1, 2, 3, 4].map(x => FileData.data[x])
Insert cell
d3.transpose(FileData.data, [1, 2, 3])
Insert cell
US = [5, 6, 7].map(x => FileData.data[x])
Insert cell
SouthAmerica=[8,9].map(x => FileData.data[x])
Insert cell
Euro = [10, 11].map(x => FileData.data[x])
Insert cell
FileData = economicData20240921 //FileAttachment("12-economic-indicators-data-2022-July-25.json").json()
Insert cell
md`## Configs`
Insert cell
height = 300
Insert cell
margin = ({ top: 20, right: 50, bottom: 25, left: 20 })
Insert cell
md`## ANX`
Insert cell
import { soFetch } from '@alecglassford/so-fetch'
Insert cell
import { select } from '@jashkenas/inputs'
Insert cell
//https://stackoverflow.com/a/34807547

function svgBBox(svgEl) {
let tempDiv = document.createElement('div');
tempDiv.setAttribute(
'style',
"position:absolute; visibility:hidden; width:0; height:0"
);
document.body.appendChild(tempDiv);
let tempSvg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
tempDiv.appendChild(tempSvg);
let tempEl = svgEl.cloneNode(true);
tempSvg.appendChild(tempEl);
let bb = tempEl.getBBox();
document.body.removeChild(tempDiv);
return bb;
}
Insert cell
curveTypes = [
{
name: 'curveLinear',
curve: d3.curveLinear,
active: true,
lineString: '',
clear: false,
info: 'Interpolates the points using linear segments.'
},
{
name: 'curveBasis',
curve: d3.curveBasis,
active: true,
lineString: '',
clear: true,
info:
'Interpolates the start and end points and approximates the inner points using a B-spline.'
},
{
name: 'curveBasisClosed',
curve: d3.curveBasisClosed,
active: false,
lineString: '',
clear: false,
info: 'Uses a closed B-Spline to approximate the points.'
},
{
name: 'curveBundle (ß=0)',
curve: d3.curveBundle.beta(0),
active: false,
lineString: '',
clear: true,
info:
'Same as curveBasis with the addition of a paramter ß which determines how close to a straight line the curve is. If ß=0 the curve is straight.'
},
{
name: 'curveBundle (ß=0.5)',
curve: d3.curveBundle.beta(0.5),
active: false,
lineString: '',
clear: false,
info:
'Same as curveBasis with the addition of a paramter ß which determines how close to a straight line the curve is.'
},
{
name: 'curveBundle (ß=1)',
curve: d3.curveBundle.beta(1),
active: false,
lineString: '',
clear: false,
info:
'Same as curveBasis with the addition of a paramter ß which determines how close to a straight line the curve is. If ß=1 the curve is the same as curveBasis.'
},
{
name: 'curveCardinal (tension=0)',
curve: d3.curveCardinal.tension(0),
active: false,
lineString: '',
clear: true,
info:
"Interpolates the points using a cubic B-spline. A tension parameter determines how 'taut' the curve is. As tension approaches 1 the segments become linear."
},
{
name: 'curveCardinal (tension=0.5)',
curve: d3.curveCardinal.tension(0.5),
active: false,
lineString: '',
clear: false,
info:
"Interpolates the points using a cubic B-spline. A tension parameter determines how 'taut' the curve is. As tension approaches 1 the segments become linear."
},
{
name: 'curveCardinal (tension=1)',
curve: d3.curveCardinal.tension(1),
active: false,
lineString: '',
clear: false,
info:
"Interpolates the points using a cubic B-spline. A tension parameter determines how 'taut' the curve is. As tension approaches 1 the segments become linear."
},
{
name: 'curveCatmullRom (α=0)',
curve: d3.curveCatmullRom.alpha(0),
active: false,
lineString: '',
clear: true,
info:
'Similar to curveCardinal (tension=0) but with a parameter α that determines the parameterisation used to interpolate the points. If α=0 the parameterisation is uniform.'
},
{
name: 'curveCatmullRom (α=0.5)',
curve: d3.curveCatmullRom.alpha(0.5),
active: false,
lineString: '',
clear: false,
info:
'Similar to curveCardinal (tension=0) but with a parameter α that determines the parameterisation used to interpolate the points. If α=0.5 the parameterisation is centripetal and self intersecting loops are avoided.'
},
{
name: 'curveCatmullRom (α=1)',
curve: d3.curveCatmullRom.alpha(1),
active: false,
lineString: '',
clear: false,
info:
'Similar to curveCardinal (tension=0) but with a parameter α that determines the parameterisation used to interpolate the points. If α=1 the parameterisation is chordal.'
},
{
name: 'curveMonotoneX',
curve: d3.curveMonotoneX,
active: false,
lineString: '',
clear: true,
info:
'Interpolates the points with a cubic spline which are monotonic (i.e. always increasing or always decreasing) in y.'
},
{
name: 'curveMonotoneY',
curve: d3.curveMonotoneY,
active: false,
lineString: '',
clear: false,
info:
'Interpolates the points with a cubic spline which are monotonic (i.e. always increasing or always decreasing) in x.'
},
{
name: 'curveNatural',
curve: d3.curveNatural,
active: false,
lineString: '',
clear: true,
info:
'Interpolates the points with a cubic spline with zero 2nd derivatives at the endpoints.'
},
{
name: 'curveStep',
curve: d3.curveStep,
active: false,
lineString: '',
clear: true,
info:
'Interpolates the points with alternating horizontal and vertical linear segments. The vertical segments lie midway between points.'
},
{
name: 'curveStepAfter',
curve: d3.curveStepAfter,
active: false,
lineString: '',
clear: false,
info:
'Interpolates the points with alternating horizontal and vertical linear segments. The y value changes after the x value.'
},
{
name: 'curveStepBefore',
curve: d3.curveStepBefore,
active: false,
lineString: '',
clear: false,
info:
'Interpolates the points with alternating horizontal and vertical linear segments. The y value changes before the x value.'
}
]
Insert cell
d3 = require("d3-array@2", 'd3@5')
Insert cell
// GenerateInfo(RegionalData['U.S.A.'][2])
Insert cell
// economicData20231121 = FileAttachment("economic-data-2023-11-21.json").json()
Insert cell
economicData20240921 = FileAttachment("bloomberg-2024-09-21-data.json").json()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more