Public
Edited
Mar 28
Insert cell
Insert cell
async function getStockMap() {
try {
const proxyUrl = 'https://api.allorigins.win/raw?url=';
const response = await fetch(proxyUrl + 'http://api.mairui.club/hslt/list/26CCBB57-BC15-479D-8EAA-668F5E6056BA');
const data = await response.json();
// 创建代码->名称的Map
return new Map(data.map(item => [item.dm, item.mc]));
} catch (error) {
console.error('获取股票列表失败:', error);
return new Map(); // 返回空Map防止后续出错
}
}
Insert cell
// 步骤2:为数据集添加gupiao列
async function addStockNameColumn() {
// 获取映射表
const stockMap = await getStockMap();
// 添加新列
const newData = data2.map(d => ({
...d, // 保留原有字段
gupiao: stockMap.get(d.name) || '未知股票' // 添加新列
}));
// 可选:过滤未知股票
// return newData.filter(d => d.gupiao !== '未知股票');
return newData;
}
Insert cell
data = await addStockNameColumn()
Insert cell
stockCodes = ['688981', '301291', '688508', '002967', '002970',
'001308', '300433', '300735', '300001', '600887']
Insert cell
proxyUrl = 'https://api.allorigins.win/raw?url='
Insert cell
async function fetchStockData(code) {
try {
const response = await fetch(proxyUrl + `http://api.mairui.club/hsmy/zhlrt/${code}/26CCBB57-BC15-479D-8EAA-668F5E6056BA
`);
// 网络层错误处理
if (!response.ok) {
throw new Error(`HTTP错误 ${response.status}`);
}
const data = await response.json();
// 数据结构验证
if (!Array.isArray(data)) {
throw new Error("API返回非数组数据");
}
return data;
} catch (error) {
console.error(`[${code}] 数据获取失败:`, error.message);
return []; // 返回空数组保持流程
}
}
Insert cell
transformEntry = (d, code) => {
try {
return {
date: d3.timeParse("%Y-%m-%d")(d.t),
name: code,
category: "1",
value: Number(d.zljlr) || 0 // 处理无效数值
};
} catch (e) {
console.warn(`[${code}] 数据转换异常:`, d);
return null;
}
}
Insert cell
finalData = await Promise.allSettled(
stockCodes.map(async code => {
const rawData = await fetchStockData(code);
return rawData
.map(d => transformEntry(d, code))
.filter(Boolean); // 过滤转换失败项
})
).then(results => {
return results
.filter(r => r.status === 'fulfilled')
.flatMap(r => r.value);
})
Insert cell
data2 = [...finalData].sort((a, b) => {
// 将日期字符串转换为时间戳进行比较
const timestampA = new Date(a.date).getTime();
const timestampB = new Date(b.date).getTime();
// 从远到近排序 (旧日期在前)
return timestampA - timestampB;
})
Insert cell
data1 = FileAttachment("category-brands.csv").csv({typed: true})
Insert cell
Insert cell
chart = {
replay;

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

const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);

yield svg.node();

for (const keyframe of keyframes) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);

// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);

updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}
}
Insert cell
duration = 250
Insert cell
n = 12
Insert cell
names = new Set(data.map(d => d.name))
Insert cell
datevalues = Array.from(d3.rollup(data, ([d]) => d.value, d => +d.date, d => d.name))
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b))
Insert cell
function rank(value) {
const data = Array.from(names, name => ({name, value: value(name)}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
Insert cell
k = 10
Insert cell
keyframes = {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank(name => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t)
]);
}
}
keyframes.push([new Date(kb), rank(name => b.get(name) || 0)]);
return keyframes;
}
Insert cell
nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)
Insert cell
Insert cell
Insert cell
function bars(svg) {
let bar = svg.append("g")
.attr("fill-opacity", 0.6)
.selectAll("rect");

return ([date, data], transition) => bar = bar
.data(data.slice(0, n), d => d.name)
.join(
enter => enter.append("rect")
.attr("fill", color)
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", d => y((prev.get(d) || d).rank))
.attr("width", d => x((prev.get(d) || d).value) - x(0)),
update => update,
exit => exit.transition(transition).remove()
.attr("y", d => y((next.get(d) || d).rank))
.attr("width", d => x((next.get(d) || d).value) - x(0))
)
.call(bar => bar.transition(transition)
.attr("y", d => y(d.rank))
.attr("width", d => x(d.value) - x(0)));
}
Insert cell
function labels(svg) {
let label = svg.append("g")
.style("font", "bold 12px var(--sans-serif)")
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.selectAll("text");

return ([date, data], transition) => label = label
.data(data.slice(0, n), d => d.name)
.join(
enter => enter.append("text")
.attr("transform", d => `translate(${x((prev.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", "-0.25em")
.text(d => d.gupiao || '未知股票')
.call(text => text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em")),
update => update,
exit => exit.transition(transition).remove()
.attr("transform", d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
.call(g => g.select("tspan").tween("text", d => textTween(d.value, (next.get(d) || d).value)))
)
.call(bar => bar.transition(transition)
.attr("transform", d => `translate(${x(d.value)},${y(d.rank)})`)
.call(g => g.select("tspan").tween("text", d => textTween((prev.get(d) || d).value, d.value))));
}
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function(t) {
this.textContent = formatNumber(i(t));
};
}
Insert cell
formatNumber = d3.format(",d")
Insert cell
tickFormat = undefined // override as desired
Insert cell
function axis(svg) {
const g = svg.append("g")
.attr("transform", `translate(0,${marginTop})`);

const axis = d3.axisTop(x)
.ticks(width / 160, tickFormat)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));

return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "white");
g.select(".domain").remove();
};
}
Insert cell
function ticker(svg) {
const now = svg.append("text")
.style("font", `bold ${barSize}px var(--sans-serif)`)
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", marginTop + barSize * (n - 0.45))
.attr("dy", "0.32em")
.text(formatDate(keyframes[0][0]));

return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
Insert cell
formatDate = d3.utcFormat("%Y")
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some(d => d.category !== undefined)) {
const categoryByName = new Map(data.map(d => [d.name, d.category]))
scale.domain(categoryByName.values());
return d => scale(categoryByName.get(d.name));
}
return d => scale(d.name);
}
Insert cell
x = d3.scaleLinear([0, 1], [marginLeft, width - marginRight])
Insert cell
y = d3.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([marginTop, marginTop + barSize * (n + 1 + 0.1)])
.padding(0.1)
Insert cell
height = marginTop + barSize * n + marginBottom
Insert cell
barSize = 48
Insert cell
marginTop = 16
Insert cell
marginRight = 6
Insert cell
marginBottom = 6
Insert cell
marginLeft = 0
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
validData = data.slice(0, number)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
debugSVG = d3.create("svg")
.attr("width", 600)
.attr("height", 400)
Insert cell
Insert cell
Insert cell
Insert cell
testData = data.slice(0, 5); // 使用前5条原始数据
Insert cell
"初始数据:"+testData.map(d => d.gupiao)
Insert cell
x2 = d3.scaleLinear()
.domain([0, d3.max(testData, d => Math.abs(d.value))])
.range([50, 550])
Insert cell
y2 = d3.scaleBand()
.domain(testData.map(d => d.name))
.range([50, 250])
.padding(0.2)
Insert cell
viewof replay3 = html`<button>Replay`
Insert cell
Insert cell
Insert cell
chart4 = {
replay;

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

const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);

yield svg.node();

for (const keyframe of keyframes) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);

// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);

updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}
}
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