Public
Edited
Nov 1, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {
epoch2human,
human2epoch,
card,
details,
timeCounter,
epochOffset,
generateDates,
timeSinceMidnight,
timeResetToMidnight,
timeLeftTillMidnight
} from "@rayraegah/got-a-date"
Insert cell
import { Calendar } from "@observablehq/calendar-component"
//import { toc } from "@nebrius/indented-toc"
Insert cell
Plot = tooltipPlugin(await require("@observablehq/plot"))
Insert cell
Insert cell
Insert cell
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
}
Insert cell
function weightedAverage(data) {
let sum = 0;
let totalWeight = 0;

for (const [category, weight] of data) {
sum += category * weight;
totalWeight += weight;
}

return sum / totalWeight;
}
Insert cell
function shareOfTarget(data, target) {
let sum = 0;
let targetSum = 0;

for (const [speed, frequency] of data) {
sum += frequency;
targetSum += speed <= target ? frequency : 0;
}

return targetSum / sum;
}
Insert cell
// Get the next business day by checking for restrictions
function nextValidDay(ts, offset = 0, type = "is_shipping_restricted") {
const dayToCheck = epochOffset(ts, offset);
const isRestricted = dict_calendar[dayToCheck][type];

return isRestricted
? nextValidDay(ts, offset + 1, type)
: epochOffset(ts, offset);
}
Insert cell
function calculateShippingDate(orderDateInSec) {
const type = "is_shipping_restricted";
// if the order is placed after cut-off time
// then it is processed on the next business day
let anchorDateInSec =
timeSinceMidnight(orderDateInSec) > configuredCutoffTime - 3600
? orderDateInSec + timeLeftTillMidnight(orderDateInSec)
: orderDateInSec - timeSinceMidnight(orderDateInSec);

// if the determined processing day is a holiday
// then it is processed on the next business day
anchorDateInSec = nextValidDay(anchorDateInSec, 0, type);

const shipByDateInSec = anchorDateInSec + configuredHandlingTime;

// if the the determined ship by date after adding cut-off time is a holiday
// then it is shipped on the next business day
const earliestShipDateInSec = nextValidDay(shipByDateInSec, 0, type);

// if the configured handling time is 0-day or 1-day
// then minimum and maximum ship by dates are the same
const latestShipDateInSec =
configuredHandlingTime > 86400
? nextValidDay(earliestShipDateInSec, 1, type)
: earliestShipDateInSec;

return {
min: earliestShipDateInSec, //+ configuredCutoffTime,
max: latestShipDateInSec //+ configuredCutoffTime
};
}
Insert cell
function calculateDeliveryDate(earliestShipDateInSec, latestShipDateInSec) {
let min = earliestShipDateInSec + defaultTransitTime;
const max = latestShipDateInSec + defaultTransitTime;

if (deliveryMethod === "Standard" && defaultTransitTime !== 24 * 3600) {
min = earliestShipDateInSec + defaultTransitTime - 24 * 3600;
}

return { min, max };
}
Insert cell
function calculatePromiseSpeed(orderDate, shipByDates, deliveryDates) {
const minLeadTime = timeCounter(shipByDates.min - orderDate);
const maxLeadTime = timeCounter(shipByDates.max - orderDate);
const minSpeed = timeCounter(deliveryDates.min - orderDate);
const maxSpeed = timeCounter(deliveryDates.max - orderDate);

return { minLeadTime, maxLeadTime, minSpeed, maxSpeed };
}
Insert cell
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
}
Insert cell
// Function to position the tooltip
hover = (tip, pos, text) => {
const side_padding = 10;
const vertical_padding = 5;
const vertical_offset = 15;

// Empty it out
tip.selectAll("*").remove();

// Append the text
tip
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("transform", `translate(${pos[0]}, ${pos[1] + 7})`)
.selectAll("text")
.data(text)
.join("text")
.style("dominant-baseline", "ideographic")
.text((d) => d)
.attr("y", (d, i) => (i - (text.length - 1)) * 15 - vertical_offset)
.style("font-weight", (d, i) => (i === 0 ? "bold" : "normal"));

const bbox = tip.node().getBBox();

// Add a rectangle (as background)
tip
.append("rect")
.attr("y", bbox.y - vertical_padding)
.attr("x", bbox.x - side_padding)
.attr("width", bbox.width + side_padding * 2)
.attr("height", bbox.height + vertical_padding * 2)
.style("fill", "white")
.style("stroke", "#d3d3d3")
.lower();
}
Insert cell
// To generate a unique ID for each chart so that they styles only apply to that chart
id_generator = () => {
var S4 = function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return "a" + S4() + S4();
}
Insert cell
addTooltips = (chart, styles) => {
const stroke_styles = { stroke: "blue", "stroke-width": 3 };
const fill_styles = { fill: "blue", opacity: 0.5 };

// Workaround if it's in a figure
const type = d3.select(chart).node().tagName;
let wrapper =
type === "FIGURE" ? d3.select(chart).select("svg") : d3.select(chart);

// Workaround if there's a legend....
const svgs = d3.select(chart).selectAll("svg");
if (svgs.size() > 1) wrapper = d3.select([...svgs].pop());
wrapper.style("overflow", "visible"); // to avoid clipping at the edges

// Set pointer events to visibleStroke if the fill is none (e.g., if its a line)
wrapper.selectAll("path").each(function (data, index, nodes) {
// For line charts, set the pointer events to be visible stroke
if (
d3.select(this).attr("fill") === null ||
d3.select(this).attr("fill") === "none"
) {
d3.select(this).style("pointer-events", "visibleStroke");
if (styles === undefined) styles = stroke_styles;
}
});

if (styles === undefined) styles = fill_styles;

const tip = wrapper
.selectAll(".hover")
.data([1])
.join("g")
.attr("class", "hover")
.style("pointer-events", "none")
.style("text-anchor", "middle");

// Add a unique id to the chart for styling
const id = id_generator();

// Add the event listeners
d3.select(chart).classed(id, true); // using a class selector so that it doesn't overwrite the ID
wrapper.selectAll("title").each(function () {
// Get the text out of the title, set it as an attribute on the parent, and remove it
const title = d3.select(this); // title element that we want to remove
const parent = d3.select(this.parentNode); // visual mark on the screen
const t = title.text();
if (t) {
parent.attr("__title", t).classed("has-title", true);
title.remove();
}
// Mouse events
parent
.on("pointerenter pointermove", function (event) {
const text = d3.select(this).attr("__title");
const pointer = d3.pointer(event, wrapper.node());
if (text) tip.call(hover, pointer, text.split("\n"));
else tip.selectAll("*").remove();

// Raise it
d3.select(this).raise();
// Keep within the parent horizontally
const tipSize = tip.node().getBBox();
if (pointer[0] + tipSize.x < 0)
tip.attr(
"transform",
`translate(${tipSize.width / 2}, ${pointer[1] + 7})`
);
else if (pointer[0] + tipSize.width / 2 > wrapper.attr("width"))
tip.attr(
"transform",
`translate(${wrapper.attr("width") - tipSize.width / 2}, ${
pointer[1] + 7
})`
);
})
.on("pointerout", function (event) {
tip.selectAll("*").remove();
// Lower it!
d3.select(this).lower();
});
});

// Remove the tip if you tap on the wrapper (for mobile)
wrapper.on("touchstart", () => tip.selectAll("*").remove());

// Define the styles
chart.appendChild(html`<style>
.${id} .has-title { cursor: pointer; pointer-events: all; }
.${id} .has-title:hover { ${Object.entries(styles)
.map(([key, value]) => `${key}: ${value};`)
.join(" ")} }`);

return chart;
}
Insert cell
tooltipPlugin = (Plot) => {
const { plot } = Plot;
Plot.plot = ({ tooltip, ...options }) => addTooltips(plot(options), tooltip);
return Plot;
}
Insert cell
Insert cell
ttOptions = {
return {
"Same day": [["0 business day", 0]],
"Next day": [["1 business day", 1 * 24 * 3600]],
"Two days": [["2 business days", 2 * 24 * 3600]],
"Standard SSA": [[]],
Standard: [
["1 business day", 24 * 3600],
["1-2 days", 2 * 24 * 3600],
["2-3 days", 3 * 24 * 3600],
["3-4 days", 4 * 24 * 3600]
]
};
}
Insert cell
// date and time of the order in seconds
orderTime = Date.parse(glanceviewLocal) / 1000
Insert cell
// calculated handling time at the time of page view in seconds
configuredHandlingTime = {
// handling time set at shipping template overrides all other settings
if (templateHandlingTime) {
return 0;
}
// handling time set on SKU overrides default setting
return skuHandlingTime || defaultHandlingTime;
}
Insert cell
// zero lead time has a different order cut-off time
// the standard order cut-off time is midnight
configuredCutoffTime = {
let cot = cutoffTimeStd;

switch (deliveryMethod) {
case "Same day":
cot = cutoffTimeSameDay;
break;
case "Next day":
cot = cutoffTimeNextDay;
break;
case "Two days":
cot = cutoffTimeTwoDay;
break;
case "Standard":
default:
cot =
defaultHandlingTime === 0 && skuHandlingTime === 0
? cutoffTimeZero
: cutoffTimeStd;
}
return cot;
}
Insert cell
jp_holidays = FileAttachment("2024-japan-holidays@1.csv").csv()
Insert cell
holiday_calendar = jp_holidays.map((d) => ({
ts: timeResetToMidnight(d.ts),
holiday_name: d.holiday,
holiday_type: d.type,
is_national_holiday: d.type === "National holiday"
}))
Insert cell
confirmedBusinessDays = overridenHolidays.map((d) => d.ts)
Insert cell
dict_calendar = {
const fullYear = generateDates("2023-01-01", "2024-12-31", true);
const calendar = {}; // [];
const overrideBusinessDay = (ts, isHoliday, weekday) => {
if (confirmedBusinessDays.includes(ts)) {
return false;
}

return isHoliday || !defaultShippingDays.includes(weekday);
};

for (let i = 0; i < fullYear.length; i++) {
const d = fullYear[i];
const dow = new Date(d.ts * 1000).getDay();
const day = {
...d,
...holiday_calendar.find((x) => x.ts === d.ts)
};

calendar[d.ts] = {
// calendar.push({
ts: d.ts,
date: day.date,
day: dow,
is_holiday: day.is_national_holiday || false,
is_shipping_restricted: overrideBusinessDay(
d.ts,
day.is_national_holiday,
dow
),
is_delivery_restricted: false
};
// });
}

return calendar;
}
Insert cell
estimatedShippingDate = calculateShippingDate(orderTime)
Insert cell
estimatedDeliveryDate = calculateDeliveryDate(
estimatedShippingDate.min,
estimatedShippingDate.max
)
Insert cell
solarized = {
return {
base03: "#002b36",
base02: "#073642",
base01: "#586e75",
base00: "#657b83",
base0: "#839496",
base1: "#93a1a1",
base2: "#eee8d5",
base3: "#fdf6e3",
yellow: "#b58900",
orange: "#cb4b16",
red: "#dc322f",
magenta: "#d33682",
violet: "#6c71c4",
blue: "#268bd2",
cyan: "#2aa198",
green: "#859900"
};
}
Insert cell
fulfillmentTimeline = Object.values(dict_calendar).map((d) => {
const events = {
[timeResetToMidnight(estimatedShippingDate.min)]: "Earliest Ship By Day",
[timeResetToMidnight(estimatedShippingDate.max)]: "Latest Ship By Day",
[timeResetToMidnight(estimatedDeliveryDate.min)]: "Earliest Delivery Day",
[timeResetToMidnight(estimatedDeliveryDate.max)]: "Latest Delivery Day",
[timeResetToMidnight(orderTime)]: "Order Day",
1: "Fulfillment Holiday",
0: "Business Day"
};

const text = (x, y) => events[x] || events[y];

return {
...d,
milestone: text(d.ts, +d.is_shipping_restricted)
};
})
Insert cell
simulationTimeline = generateDates("2023-01-01", "2023-12-31", true).map(
(d) => {
const randomTime = (t) => {
let rnd = 0;
const seed = [
getRandomInt(0, configuredCutoffTime - 3600),
getRandomInt(configuredCutoffTime, 23 * 3600)
];
switch (simulateSpeed) {
case 0:
// before
rnd = seed[0];
break;
case 1:
// before
rnd = seed[1];
break;
case 2:
// randomly
rnd = seed[Math.round(Math.random())];
break;
default:
rnd = 0;
}

return rnd;
};

const t0 = d.ts + randomTime();
const t1 = calculateShippingDate(t0);
const t2 = calculateDeliveryDate(t1.min, t1.max);

const speed = calculatePromiseSpeed(t0, t1, t2);
return { ...d, t0, t1, t2, speed: speed.maxSpeed.days };
}
)
Insert cell
// d3.rollup(athletes, v => d3.sum(v, d => d.earnings), d => d.sport)
daysBySpeed = d3.rollups(
simulationTimeline,
(v) => d3.count(v, (d) => d.ts),
(d) => d.speed
)
Insert cell
debug = {
return {
order: epoch2human(orderTime),
minShip: epoch2human(estimatedShippingDate.min),
maxShip: epoch2human(estimatedShippingDate.max),
minPromise: epoch2human(estimatedDeliveryDate.min),
maxPromise: epoch2human(estimatedDeliveryDate.max),
handlingTime: timeCounter(configuredHandlingTime),
cutoffTime: timeCounter(configuredCutoffTime),
transitTime: timeCounter(defaultTransitTime),
...calculatePromiseSpeed(
orderTime,
estimatedShippingDate,
estimatedDeliveryDate
)
};
}
Insert cell
Insert cell
<style>
div.observablehq form {
--label-width: 150px;
}
</style>
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