Public
Edited
Sep 28, 2023
Insert cell
Insert cell
iTable(data_ev_sans_id, {"rows":19,
"header": evDict})
Insert cell
iTable = (data,
{
columns = maybeColumns(data), // array of column names
value, // initial selection
required = true, // if true, the value is everything if nothing is selected
sort, // name of column to sort by, if any
reverse = false, // if sorting, true for descending and false for ascending
format, // object of column name to format function
locale,
align, // object of column name to left, right, or center
header, // object of column name to string or HTML element
rows = 11.5, // maximum number of rows to show
height,
maxWidth,
width = {}, // object of column name to width, or overall table width
multiple = true,
layout = "auto" // "fixed" or "auto"
} = {}
) => {
// Deep copy original data to be displayed at all times in the summary charts
const dataFull = JSON.parse(JSON.stringify(data));
const rowHeight = 22;
const maxHeight = height === undefined ? (rows + 1) * rowHeight - 1 : undefined;

columns = columns === undefined ? columnsof(data) : arrayify(columns);
format = myFormatof(format, data, columns, locale);

let colNames;
let index = Uint32Array.from(data, (_, i) => i);
let n = rows * 2;

console.log('rows: ', rows)
// Compose the table html
const tbody = html`<tbody>`;
const tr = html`<tr><td></td>${columns.map(
column => html`<td style="text-align:left;">`
)}`;
// Construct <theader>
const theadr = html`<tr>
<th></th>
${columns.map(column => {
let thisChart = iheaderCharts(dataFull, column);
console.log('thisChart.value: ', thisChart.value)
if (thisChart.value) {
console.log('thisChart generator: ', Generators.input(thisChart))
console.log('thisChart.value[mean]: ', thisChart.value["mean"])
console.log('formatted thisChart.value[mean]: ', d3.format("d")(thisChart.value["mean"]))
}
return html`<th title=${column} style="text-align:left;"><span></span>
<div>${header && column in header ? header[column] : column}</div>
<div class="stats">${thisChart.value ?
`Average: ${d3.format("d")(thisChart.value["mean"])}`
: ''}
</div>
<div>${thisChart}</div>
</th>`
})}
</tr>`;
// Construct <tbody>
const element = html`<div class=${id} style="max-height: ${(rows + 1) * 24 - 1}px">
<table style="table-layout:${layout};">
<thead>${data.length || columns.length ? theadr : null}</thead>
${tbody}
</table>
</div>`;
function render(i, j) {
return Array.from(index.subarray(i, j), i => {
const itr = tr.cloneNode(true);
//itr.classList.toggle("selected", selected.has(i));
for (let j = 0; j < columns.length; ++j) {
let column = columns[j];
let value = data[i][column];
if (value === null && !Number.isNaN(value)) continue;
value = format[column](value); //value; //
if (!(value instanceof Node)) value = document.createTextNode(value);
itr.childNodes[j + 1].appendChild(value);
}
return itr;
});
}
tbody.append(...render(0, n));
element.value = value;
return element
}
Insert cell
// FOR THIS EXERCISE ONLY CONSIDER THE HISTOGRAM
// chart = d3HistogramFn(data, col)
iheaderCharts = (data, col) => {
let content, value, missing_label, pct_missing, min, max, median, mean, sd;
const notFiniteFormat = d3.format(",.0f");
let format;
let chart;
let el;
// Construct chart based on column type
const type = getType(data, col)

switch(type) {
// Categorical columns ***** IGNORE FOR NOW *******
case 'ordinal':
//format = d3.format(",.0f")
// Calculate category percent and count
const categories = d3.rollups(
data,
v => ({count:v.length, pct:v.length / data.length || 1}),
d => d[col]
).sort((a, b) => b[1].count - a[1].count)
.map(d => {
let obj = {}
obj[col] = (d[0] === null || d[0] === "") ? "(missing)" : d[0]
obj.count = d[1].count
obj.pct = d[1].pct
return obj
})
// Create the chart
const stack_chart = SmallStack(categories, col)

// element to return
el = htl.html`
<td><div style="width:150px;padding:5px;">${stack_chart}</div></td>`
break;
// Date columns ***** IGNORE FOR NOW *******
case "date":
// Element to return
el = htl.html`
<td><div style="width:150px;padding:5px;">${col} ${type} CHART</div></td>`
break;
// Continuous columns ***** HISTOGRAM FUNCTION HERE *******
default:
// Compute values
min = d3.min(data, d => +d[col])
max = d3.max(data, d => +d[col])
mean = d3.mean(data, d => +d[col])
median = d3.median(data, d => +d[col])
sd = d3.deviation(data, d => +d[col])
if(Number.isFinite(sd)) {
let finiteFormat = d3.format(",." + d3.precisionFixed(sd / 10) + "f");
format = x => Number.isFinite(x) ? finiteFormat(x) : notFiniteFormat(x);
}
else {
format = notFiniteFormat;
}
pct_missing = data.filter(d => d[col] === null || isNaN(d[col])).length / data.length

let loadData = function(selectedRegion) {
console.log('loadData selectedRegion: ', selectedRegion);
return selectedRegion;
}
chart = d3HistogramFn(data, col)

el = htl.html`
<td><div style="width:150px;padding:5px;">${chart}</div></td>`
value = {column: col, type, min, max, mean, median, sd, missing:pct_missing, n_categories:null}
break;
}
el.value = value;
el.appendChild(html`<style>td {vertical-align:middle;} </style>`)

return el;
}
Insert cell
// Stand-alone histogram test
testHist = d3HistogramFn(data_ev_clean, 'BEV')
Insert cell
// Would be nice if I can call this generator on each chart in the HTML table to get the brush values, but the generator only works when it's in a stand-alone cell as far as I can tell...
Generators.input(testHist)
Insert cell
d3HistogramFn = (data, col,
{ // settings
//type = "continuous",
margin = ({top: 20, right: 20, bottom: 30, left: 40}),
width = 250,
height = 95,
binThreshold = 10
} = {}
) => {
// Sources:
// https://observablehq.com/@d3/histogram/2?intent=fork
// https://observablehq.com/@d3/focus-context

let brushLims;


// Bin the data.
const bins = d3.bin()
.thresholds(binThreshold)
.value((d) => d[col])
(data);

// Declare the x (horizontal position) scale.
const x = d3.scaleLinear()
.domain([bins[0].x0, bins[bins.length - 1].x1])
.range([margin.left, width - margin.right]);

// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, d3.max(bins, (d) => d.length)])
.range([height - margin.bottom, margin.top]);

// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// Add a rect for each bin.
svg.append("g")
.attr("fill", "steelblue")
.selectAll()
.data(bins)
.join("rect")
.attr("x", (d) => x(d.x0) + 1)
.attr("width", (d) => x(d.x1) - x(d.x0) - 1)
.attr("y", (d) => y(d.length))
.attr("height", (d) => y(0) - y(d.length));

// Add the x-axis and label.
svg.append("g")
.style("font", "20px arial")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x)
.ticks(width / 80)
.tickSizeOuter(0)
.tickFormat(d3.format("~s"))
);

// Define brush
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, height - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);

const defaultSelection = [x.domain()[1], x.range()[1]];

// Append brush
const gb = svg.append("g")
.call(brush)
.call(brush.move, defaultSelection);

// Brush functions
function brushed({selection}) {
console.log('selection: ', selection)
if (selection) {
console.log('brush selection.map: ', selection.map(x.invert, x))
brushLims = selection.map(x.invert, x); // ********** HOW TO GET THESE BRUSH VALUES OUT?????? *******
svg.property("value", selection.map(x.invert, x));
svg.dispatch("input");
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}

// Return the SVG element.
return svg.node();
}
Insert cell
Insert cell
testTable = {
const settings = {
"margin": {top: 20, right: 20, bottom: 30, left: 40},
"width": 250,
"height": 95,
"binThreshold": 10
}
let data = data_ev_sans_id;
let columns = maybeColumns(data) === undefined ? columnsof(data) : arrayify(columns);
let column = 'BEV';
const rows = 20;
let n = rows * 2;
let index = Uint32Array.from(data, (_, i) => i); // array of numbers from 0...len(data)
console.log('index: ', index)

let format;
let locale;
format = myFormatof(format, data, columns, locale);

let thisChart = iScatterView; //iHistView;

//table header
let theadr = html`<table style="table-layout:auto;">
<thead>
<tr>
<th></th>
<th title='title' style="text-align:left;"><span></span>
<div>${column}</div>
<div>${thisChart}</div>
</th>
</tr>
</thead>
</table>`;

// Construct <tbody>
let rowData = filteredScatterData.length !== 0 ? filteredScatterData : data;
let rowIndex = Uint32Array.from(rowData, (_, i) => i);
const tbody = html`<tbody>`;
const tr = html`<tr><td></td>${columns.map(
column => html`<td style="text-align:left;">`
)}`;
const element = html`<div class=${id} style="max-height: ${(rows + 1) * 24 - 1}px">
<table style="table-layout:fixed;">
<thead>${data.length || columns.length ? theadr : null}</thead>
${tbody}
</table>
</div>`;

function render(i, j) {
return Array.from(rowIndex.subarray(i, j), i => {
const itr = tr.cloneNode(true);
//itr.classList.toggle("selected", selected.has(i));
for (let j = 0; j < columns.length; ++j) {
let column = columns[j];
let value = data[i][column];
if (value === null && !Number.isNaN(value)) continue;
value = format[column](value); //value; //
if (!(value instanceof Node)) value = document.createTextNode(value);
itr.childNodes[j + 1].appendChild(value);
}
return itr;
});
}
tbody.append(...render(0, n));

return element;
}
Insert cell
filteredScatterData = Generators.observe(next => {
// Yield the input’s initial value.
next(iScatterView.value);

// Define an event listener to yield the input’s next value.
const inputted = () => next(iScatterView.value);

// Attach the event listener.
iScatterView.addEventListener("input", inputted);

// When the generator is disposed, detach the event listener.
return () => iScatterView.removeEventListener("input", inputted);
})
Insert cell
iScatterView = {

// Specify the chart’s dimensions.
const width = 928;
const height = 600;
const marginTop = 20;
const marginRight = 30;
const marginBottom = 30;
const marginLeft = 40;

const data = data_ev_sans_id;
const xcol = 'BEV';
const ycol = 'TotalEV';

// Create the horizontal (x) scale, positioning N/A values on the left margin.
const x = d3.scaleLinear()
.domain([0, d3.max(data, d => d[xcol])]).nice()
.range([marginLeft, width - marginRight])
.unknown(marginLeft);

// Create the vertical (y) scale, positioning N/A values on the bottom margin.
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d[ycol])]).nice()
.range([height - marginBottom, marginTop])
.unknown(height - marginBottom);

// Create the SVG container.
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.property("value", []);

// Append the axes.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", width - marginRight)
.attr("y", -4)
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "end")
.text(xcol));

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 4)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(ycol));

// Append the dots.
const dot = svg.append("g")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(data)
.join("circle")
.attr("transform", d => `translate(${x(d[xcol])},${y(d[ycol])})`)
.attr("r", 3);

// Create the brush behavior.
svg.call(d3.brush().on("start brush end", ({selection}) => {
let value = [];
console.log('iHistView selection: ', selection)
if (selection) {
const [[x0, y0], [x1, y1]] = selection;
value = dot
.style("stroke", "gray")
.filter(d => x0 <= x(d[xcol]) && x(d[xcol]) < x1
&& y0 <= y(d[ycol]) && y(d[ycol]) < y1)
.style("stroke", "steelblue")
.data();
} else {
dot.style("stroke", "steelblue");
}

// Inform downstream cells that the selection has changed.
svg.property("value", value).dispatch("input");
}));

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
data_pssd_clean
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
SummaryTable(
data_pssd_clean,
{},
{label: "Summary Statistics"}
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Create a view of the summary statistics of only the numeric columns of the dataset
viewof summary_data_ev = SummaryTable(
data_ev_sans_id,
evDict,
{label: "Summary Statistics"}
)
Insert cell
Insert cell
Insert cell
viewof intensity = Inputs.range([0, 100], {step: 1, label: "Intensity"})
Insert cell
Inputs.range([0, 100], {step: 1, label: "Intensity"})
Insert cell
Insert cell
Insert cell
id = DOM.uid("table").id
Insert cell
Insert cell
Insert cell
// const theadr = html`<tr>
// ${columns.map((column) => html`<th title=${column} onclick=${event => resort(event, column)}><span></span>${header && column in header ? header[column] : column}</th>`)}</tr>`;
// root.appendChild(html.fragment`<table style=${{tableLayout: layout}}>
Insert cell
Insert cell
// Column Name dictionary for Summary Table snapshots
evDict = {
let obj = {}
obj['BEV'] = 'Battery EVs';
obj['PHEV'] = 'Plug-in Hybrid EVs';
obj['TotalEV'] = 'Total EVs';
obj['FSA'] = 'Forward Sortation Area';

return obj;
}
Insert cell
Insert cell
Insert cell
Insert cell
api_pssd = d3.json(url_pssd);
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data_pssd_clean = data_pssd.map(obj => {
return { ...obj,
Salary: Number(obj['Salary'].replace("$",'').trim().replace(",",'')),
Benefits: Number(obj['Salary'].replace("$",'').trim().replace(",",''))
};;
});
Insert cell
// Add the correct data types to each column
data_ev_clean = data_ev.map(obj => {
return { ...obj,
BEV: parseInt(obj['BEV']),
PHEV: parseInt(obj['PHEV']),
TotalEV: parseInt(obj['TotalEV'])
};;
});
Insert cell
// Return the data array without the _id element in each object
data_ev_sans_id = data_ev_clean.map(o => Object.fromEntries(
headerCols(data_ev_clean[0]).map(k => [k, o[k]])
))
Insert cell
// Return the data array without the _id element in each object
data_pssd_sans_id = data_pssd_clean.map(o => Object.fromEntries(
headerCols(data_pssd_clean[0]).map(k => [k, o[k]])
))
Insert cell
Insert cell
// Returns numeric column header names excluding "_id"
summary_cols = function (o) {
const headers = Object.keys(o);

let numeric_headers = [];
headers.forEach(function(el) {
if ( typeof(o[el]) === "number" && el !== "_id") {
numeric_headers.push(el);
};
})
return numeric_headers;
};
Insert cell
// Returns numeric column header names excluding "_id"
headerCols = function (o) {
const headers = Object.keys(o);

let returnHeaders = [];
headers.forEach(function(el) {
if ( el !== "_id") {
returnHeaders.push(el);
};
})
return returnHeaders;
};
Insert cell
Insert cell
//d3 = require('d3@5')
Insert cell
//import { SummaryTable } from "@observablehq/summary-table"
Insert cell
Insert cell
pct_format = d3.format(".1%");
Insert cell
Insert cell
// Function that returns a summary table
SummaryTable = (dataObj, colDict, {label="Summary"} = {}) => {
const data = typeof dataObj.numRows === "function" ? dataObj.objects() :
typeof dataObj.toArray === "function" ? dataObj.toArray().map((r) => Object.fromEntries(r)) :
dataObj
const sample = data[0] || {};
const cols = data.columns || Object.keys(sample);
let value = []

// Create the summary card and track data shape
// const summaryCard = SummaryCard(data, label)
// value.n_rows = summaryCard.value.n_rows
// value.n_columns = summaryCard.value.n_columns
// value.columns = cols

// Compose the element
const element = htl.html`
<div style="display:inline-block; max-width:${width < 500 ? width : width - 160}px">
<table style="vertical-align:middle; display:block;overflow-x:auto; max-width:${width}px;">
<thead style="z-index:-999;">
<th>Column</th>
<th style="min-width:250px">Snapshot</th>
<th>Missing</th>
<th>Mean</th>
<th>Median</th>
<th>SD</th>
</thead>
${cols.map(d => {
const ele = SummarizeColumn(data, d, colDict)
value.push(ele.value) // get the value from the element
return ele
})}
</table>
</div>`
element.value = value;
return element
}
Insert cell
// A function to summarize a single column
SummarizeColumn = (data, col, colDict) => {
let content, value, format, finiteFormat, el, chart, missing_label, pct_missing, min, max, median, mean, sd, colName;
const notFiniteFormat = d3.format(",.0f");

// Construct content based on type
const type = getType(data, col)

// Column name to display
if (JSON.stringify(colDict) === "{}") {
colName = col
} else {
colName = colDict[col]
}
const col1 = htl.html`<td style="white-space: nowrap;vertical-align:middle;padding-right:5px;padding-left:3px;"><strong style="vertical-align:middle;">${col === "" ? "unlabeled" : colName}</strong></td>`
switch(type) {
// Categorical columns
case 'ordinal':
format = d3.format(",.0f")
// Calculate category percent and count
const categories = d3.rollups(
data,
v => ({count:v.length, pct:v.length / data.length || 1}),
d => d[col]
).sort((a, b) => b[1].count - a[1].count)
.map(d => {
let obj = {}
obj[col] = (d[0] === null || d[0] === "") ? "(missing)" : d[0]
obj.count = d[1].count
obj.pct = d[1].pct
return obj
})
// Calculate pct. missing
pct_missing = data.filter(d => (d[col] === null || d[col] === "")).length / data.length
// Create the chart
const stack_chart = SmallStack(categories, col)
// element to return
el = htl.html`<tr style="font-family:sans-serif;font-size:13px;">
${col1}
<td><div style="position:relative;">${stack_chart}</div></td>
<td>${pct_format(pct_missing)}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>`;
value = {column: col, type, min:null, max: null, mean: null, median: null,
sd: null, missing:pct_missing, n_categories:categories.length}
break;
// Date columns
case "date":
// Calculate and format start / end
const start = d3.min(data, d => +d[col])
const end = d3.max(data, d => +d[col])
mean = d3.mean(data, d => +d[col]);
median = d3.median(data, d => +d[col]);
sd = d3.deviation(data, d => +d[col]);
// Calculate pct. missing
pct_missing = data.filter(d => d[col] === null || d[col] === "").length / data.length
chart = Histogram(data, col, type)
// Element to return
el = htl.html`<tr style="font-family:sans-serif;font-size:13px;">
${col1}
<td><div style="position:relative;">${chart}</div></td>
<td>${pct_format(pct_missing)}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>`
value = {column: col, type, min:start, max: end, mean: null, median: null,
sd: null, missing:pct_missing, n_categories:null}
break;
// Continuous columns
default:
// Compute values
min = d3.min(data, d => +d[col])
max = d3.max(data, d => +d[col])
mean = d3.mean(data, d => +d[col])
median = d3.median(data, d => +d[col])
sd = d3.deviation(data, d => +d[col])
if(Number.isFinite(sd)) {
finiteFormat = d3.format(",." + d3.precisionFixed(sd / 10) + "f");
format = x => Number.isFinite(x) ? finiteFormat(x) : notFiniteFormat(x);
}
else {
format = notFiniteFormat;
}
pct_missing = data.filter(d => d[col] === null || isNaN(d[col])).length / data.length
chart = Histogram(data, col, type)
// Element to return
el = htl.html`<tr style="font-family:sans-serif;font-size:13px;">
${col1}
<td><div style="position:relative;top:3px;">${chart}</div></td>
<td>${pct_format(pct_missing)}</td>
<td>${format(mean)}</td>
<td>${format(median)}</td>
<td>${format(sd)}</td>
</tr>`
value = {column: col, type, min, max, mean, median, sd, missing:pct_missing, n_categories:null}
break;
}
el.value = value;
el.appendChild(html`<style>td {vertical-align:middle;} </style>`)
console.log('return el HERE: ', el.value)
return el
}
Insert cell
colorMap = new Map([["ordinal","rgba(78, 121, 167, 1)"],
["continuous", "rgba(242, 142, 44, 1)"],
["date", "rgba(225,87,89, 1)"]
].map(d => {
const col = d3.color(d[1])
const color_copy = _.clone(col)
color_copy.opacity = .6
return [d[0], {color:col.formatRgb(), brighter:color_copy.formatRgb()}]
}))
Insert cell
SmallStack = (categoryData, col, maxCategories = 100) => {
// Get a horizontal stacked bar
const label = categoryData.length === 1 ? " category" : " categories";
let chartData = categoryData;
let categories = 0;
if (chartData.length > maxCategories) {
chartData = categoryData.filter((d, i) => i < maxCategories);
const total = d3.sum(categoryData, (d) => d.count);
const otherCount = total - d3.sum(chartData, (d) => d.count);
let other = {};
other[col] = "Other categories...";
other.count = otherCount;
other.pct = other.count / total;
chartData.push(other);
}

return addTooltips(
Plot.barX(chartData, {
x: "count",
fill: col,
y: 0,
title: (d) => d[col] + "\n" + pct_format(d.pct)
}).plot({
color: { scheme: "blues" },
marks: [
Plot.text([0, 0], {
x: 0,
frameAnchor: "bottom",
dy: 10,
text: (d) => d3.format(",.0f")(categoryData.length) + `${label}`
})
],
style: {
paddingTop: "0px",
paddingBottom: "15px",
textAnchor: "start",
overflow: "visible"
},
x: { axis: null },
color: {
domain: chartData.map((d) => d[col]),
scheme: "blues",
reverse: true
},
height: 60,
width: 205,
y: {
axis: null,
range: [30, 3]
}
}),
{ fill: "darkblue" }
);
}
Insert cell
getType = (data, column) => {
for (const d of data) {
const value = d[column];
if (value == null) continue;
if (typeof value === "number") return "continuous";
if (value instanceof Date) return "date";
return "ordinal"
}
// if all are null, return ordinal
return "ordinal"
}
Insert cell
Insert cell
Insert cell
Insert cell
import {rangeSlider} from "@bumbeishvili/utils"
Insert cell
import {histogramSlider} from "@trebor/snapping-histogram-slider"
Insert cell
viewof v = rangeSlider(data_ev_clean, d=>d.BEV)
Insert cell
Insert cell
myhist = d3HistogramFn(data_ev_clean, 'BEV') //brushRange = Generators.input(myhist)
Insert cell
brushRange = Generators.input(myhist)
Insert cell
data_ev_sans_id.filter(res=>(res.BEV > 200 && res.BEV < 400))
Insert cell
Insert cell
iTable(data_pssd_sans_id, {"rows":19, "layout":"fixed"})
Insert cell
Insert cell
function iterable(array) {
return array ? typeof array[Symbol.iterator] === "function" : false;
}
Insert cell
function maybeColumns(data) {
if (iterable(data.columns)) return data.columns; // d3-dsv, FileAttachment
if (data.schema && iterable(data.schema.fields)) return Array.from(data.schema.fields, f => f.name); // apache-arrow
if (typeof data.columnNames === "function") return data.columnNames(); // arquero
}
Insert cell
function columnsof(data) {
const columns = new Set();
for (const row of data) {
for (const name in row) {
columns.add(name);
}
}
return Array.from(columns);
}
Insert cell
function arrayify(array) {
return Array.isArray(array) ? array : Array.from(array);
}
Insert cell
function formatof(base = {}, data, columns, locale) {
const format = Object.create(null);
for (const column of columns) {
if (column in base) {
format[column] = base[column];
continue;
}
switch (type(data, column)) {
case "number" && column.toLowerCase().indexOf('year') >= 0: format[column] = myFormatYear; break;
//case "number" && column.toLowerCase().indexOf('year') < 0: format[column] = formatLocaleNumber(locale); break;
case "date": format[column] = formatDate; break;
default: format[column] = formatLocaleAuto(locale); break;
}
}
return format;
}
Insert cell
function myFormatof(base = {}, data, columns, locale, isYear) {
const format = Object.create(null);
for (const column of columns) {
let thisType = inferColumnType(data, column);
if (column in base) {
format[column] = base[column];
continue;
}
switch (thisType) {
case "year" : format[column] = myFormatYear; break;
case "number": format[column] = formatLocaleNumber(locale); break;
case "date": format[column] = formatDate; break;
default: format[column] = formatLocaleAuto(locale); break;
}
}
return format;
}
Insert cell
function localize(f) {
let key = localize, value;
return (locale = "en") => locale === key ? value : (value = f(key = locale));
}
Insert cell
function type(data, column) {
for (const d of data) {
if (d == null) continue;
const value = d[column];
if (value == null) continue;
if (typeof value === "number") return "number";
if (value instanceof Date) return "date";
return;
}
}
Insert cell
function inferColumnType(data, column) {
for (const d of data) {
if (d == null) continue;
const value = d[column];
if (value == null) continue;
if (typeof value === "number" && column.toLowerCase().indexOf('year') >= 0) {
return "year";
}
if (typeof value === "number") {
return "number";
}
if (value instanceof Date) return "date";
return;
}
}
Insert cell
formatLocaleAuto = localize(locale => {
const formatNumber = formatLocaleNumber(locale);
return value => value == null ? ""
: typeof value === "number" ? formatNumber(value)
: value instanceof Date ? formatDate(value)
: `${value}`;
});
Insert cell
formatLocaleNumber = localize(locale => {
return value => value === 0 ? "0" : value.toLocaleString(locale); // handle negative zero
});
Insert cell
// https://observablehq.com/@mbostock/a-note-on-date-inputs
function formatDate(date) {
return [
(date.getFullYear()).toString().padStart(4, "0"),
(date.getMonth() + 1).toString().padStart(2, "0"),
(date.getDate()).toString().padStart(2, "0")
].join("-")
}
Insert cell
function myFormatYear(year) {
return year;
}
Insert cell
Insert cell
filteredData = Generators.observe(next => {
// Yield the input’s initial value.
next(iHistView.value);

// Define an event listener to yield the input’s next value.
const inputted = () => next(iHistView.value);

// Attach the event listener.
iHistView.addEventListener("input", inputted);

// When the generator is disposed, detach the event listener.
return () => iHistView.removeEventListener("input", inputted);
})
Insert cell
iHistView = {

// Specify the chart’s dimensions.
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 250;
const height = 95;
const binThreshold = 10;

const data = data_ev_sans_id;
const col = 'BEV';

// Sources:
// https://observablehq.com/@d3/histogram/2?intent=fork
// https://observablehq.com/@d3/focus-context

// Bin the data.
const bins = d3.bin()
.thresholds(binThreshold)
.value((d) => d[col])
(data);

// Declare the x (horizontal position) scale.
const x = d3.scaleLinear()
.domain([bins[0].x0, bins[bins.length - 1].x1])
.range([margin.left, width - margin.right]);

// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, d3.max(bins, (d) => d.length)])
.range([height - margin.bottom, margin.top]);

// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// Add a rect for each bin.
const bar = svg.append("g")
.attr("fill", "steelblue")
.selectAll()
.data(bins)
.join("rect")
.attr("x", (d) => x(d.x0) + 1)
.attr("width", (d) => x(d.x1) - x(d.x0) - 1)
.attr("y", (d) => y(d.length))
.attr("height", (d) => y(0) - y(d.length));

// Add the x-axis and label.
svg.append("g")
.style("font", "20px arial")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x)
.ticks(width / 80)
.tickSizeOuter(0)
.tickFormat(d3.format("~s"))
);


// Define brush
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, height - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);

const defaultSelection = [x.domain()[1], x.range()[1]];

// Append brush
const gb = svg.append("g")
.call(brush)
.call(brush.move, defaultSelection);

// Brush functions
function brushed({selection}) {
if (selection) {
console.log('brush selection.map: ', selection.map(x.invert, x))
svg.property("value", selection.map(x.invert, x));
svg.dispatch("input");
} else {
bar.style("stroke", "steelblue");
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}

// Return the SVG element.
return svg.node();

return svg.node();
}
Insert cell
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