将 observable d3.js 画笔和焦点示例转换为 jsfiddle

Convert observable d3.js brush and focus example to jsfiddle

我无法将此可观察示例转换为 jsfiddle。

https://observablehq.com/@d3/focus-context?collection=@d3/d3-brush

这是我的 jsfiddle https://jsfiddle.net/u5g1ychz/1/

编辑:最近的 fiddle 尝试 https://jsfiddle.net/03eagvxk/

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
</head>
<body>
  <h1>My Chart</h1>
  <div id="my_chart"></div>
</body>
</html>

CSS

.line {
    fill: none;
    stroke: #ffab00;
    stroke-width: 1.5;
}
  
.overlay {
  fill: none;
  pointer-events: all;
}

/* Style the dots by assigning a fill and stroke */
.dot {
    fill: #ffab00;
    stroke: #fff;
}
  
  .focus circle {
  fill: none;
  stroke: steelblue;
}

Javascript

/* new data */
var x = d3.timeDays(new Date(2010, 06, 01), new Date(2020, 10, 30));
var y = Array.from({length: x.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = x.map((v, i) => {
  return {
    "date": v,
    "close": y[i]
  }
});

/* begin observable code */
viewof focus = {
  const svg = d3.create("svg")
      .attr("viewBox", [0, 0, width, focusHeight])
      .style("display", "block");

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

  const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];

  svg.append("g")
      .call(xAxis, x, focusHeight);

  svg.append("path")
      .datum(data)
      .attr("fill", "steelblue")
      .attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));

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

  function brushed({selection}) {
    if (selection) {
      svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
      svg.dispatch("input");
    }
  }

  function brushended({selection}) {
    if (!selection) {
      gb.call(brush.move, defaultSelection);
    }
  }

  return svg.node();
}

update = {
  const [minX, maxX] = focus;
  const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
  chart.update(x.copy().domain(focus), y.copy().domain([0, maxY]));
}

area = (x, y) => d3.area()
    .defined(d => !isNaN(d.value))
    .x(d => x(d.date))
    .y0(y(0))
    .y1(d => y(d.value))
    
x = d3.scaleUtc()
    .domain(d3.extent(data, d => d.date))
    .range([margin.left, width - margin.right])
    
y = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([height - margin.bottom, margin.top])
    
xAxis = (g, x, height) => g
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
    
yAxis = (g, y, title) => g
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y))
    .call(g => g.select(".domain").remove())
    .call(g => g.selectAll(".title").data([title]).join("text")
        .attr("class", "title")
        .attr("x", -margin.left)
        .attr("y", 10)
        .attr("fill", "currentColor")
        .attr("text-anchor", "start")
        .text(title))
        
margin = ({top: 20, right: 20, bottom: 30, left: 40})
height = 440
focusHeight = 100

已下载可观察 javascript 文件

// https://observablehq.com/@d3/focus-context@326
export default function define(runtime, observer) {
  const main = runtime.module();
  const fileAttachments = new Map([["aapl.csv",new URL("./files/de259092d525c13bd10926eaf7add45b15f2771a8b39bc541a5bba1e0206add4880eb1d876be8df469328a85243b7d813a91feb8cc4966de582dc02e5f8609b7",import.meta.url)]]);
  main.builtin("FileAttachment", runtime.fileAttachments(name => fileAttachments.get(name)));
  main.variable(observer()).define(["md"], function(md){return(
md`# Focus + Context

This [area chart](/@d3/area-chart) uses brushing to specify a focused area. Drag the gray region to pan, or brush to zoom. Compare to a [zoomable chart](/@d3/zoomable-area-chart). Data: [Yahoo Finance](https://finance.yahoo.com/lookup)`
)});
  main.variable(observer("chart")).define("chart", ["d3","width","height","DOM","margin","data","xAxis","yAxis","area"], function(d3,width,height,DOM,margin,data,xAxis,yAxis,area)
{
  const svg = d3.create("svg")
      .attr("viewBox", [0, 0, width, height])
      .style("display", "block");

  const clipId = DOM.uid("clip");

  svg.append("clipPath")
      .attr("id", clipId.id)
    .append("rect")
      .attr("x", margin.left)
      .attr("y", 0)
      .attr("height", height)
      .attr("width", width - margin.left - margin.right);

  const gx = svg.append("g");

  const gy = svg.append("g");

  const path = svg.append("path")
      .datum(data)
      .attr("clip-path", clipId)
      .attr("fill", "steelblue");

  return Object.assign(svg.node(), {
    update(focusX, focusY) {
      gx.call(xAxis, focusX, height);
      gy.call(yAxis, focusY, data.y);
      path.attr("d", area(focusX, focusY));
    }
  });
}
);
  main.variable(observer("viewof focus")).define("viewof focus", ["d3","width","focusHeight","margin","x","xAxis","data","area","y"], function(d3,width,focusHeight,margin,x,xAxis,data,area,y)
{
  const svg = d3.create("svg")
      .attr("viewBox", [0, 0, width, focusHeight])
      .style("display", "block");

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

  const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];

  svg.append("g")
      .call(xAxis, x, focusHeight);

  svg.append("path")
      .datum(data)
      .attr("fill", "steelblue")
      .attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));

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

  function brushed({selection}) {
    if (selection) {
      svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
      svg.dispatch("input");
    }
  }

  function brushended({selection}) {
    if (!selection) {
      gb.call(brush.move, defaultSelection);
    }
  }

  return svg.node();
}
);
  main.variable(observer("focus")).define("focus", ["Generators", "viewof focus"], (G, _) => G.input(_));
  main.variable(observer("update")).define("update", ["focus","d3","data","chart","x","y"], function(focus,d3,data,chart,x,y)
{
  const [minX, maxX] = focus;
  const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
  chart.update(x.copy().domain(focus), y.copy().domain([0, maxY]));
}
);
  main.variable(observer("data")).define("data", ["d3","FileAttachment"], async function(d3,FileAttachment){return(
Object.assign(d3.csvParse(await FileAttachment("aapl.csv").text(), d3.autoType).map(({date, close}) => ({date, value: close})), {y: "↑ Close $"})
)});
  main.variable(observer("area")).define("area", ["d3"], function(d3){return(
(x, y) => d3.area()
    .defined(d => !isNaN(d.value))
    .x(d => x(d.date))
    .y0(y(0))
    .y1(d => y(d.value))
)});
  main.variable(observer("x")).define("x", ["d3","data","margin","width"], function(d3,data,margin,width){return(
d3.scaleUtc()
    .domain(d3.extent(data, d => d.date))
    .range([margin.left, width - margin.right])
)});
  main.variable(observer("y")).define("y", ["d3","data","height","margin"], function(d3,data,height,margin){return(
d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([height - margin.bottom, margin.top])
)});
  main.variable(observer("xAxis")).define("xAxis", ["margin","d3","width"], function(margin,d3,width){return(
(g, x, height) => g
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
)});
  main.variable(observer("yAxis")).define("yAxis", ["margin","d3"], function(margin,d3){return(
(g, y, title) => g
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y))
    .call(g => g.select(".domain").remove())
    .call(g => g.selectAll(".title").data([title]).join("text")
        .attr("class", "title")
        .attr("x", -margin.left)
        .attr("y", 10)
        .attr("fill", "currentColor")
        .attr("text-anchor", "start")
        .text(title))
)});
  main.variable(observer("margin")).define("margin", function(){return(
{top: 20, right: 20, bottom: 30, left: 40}
)});
  main.variable(observer("height")).define("height", function(){return(
440
)});
  main.variable(observer("focusHeight")).define("focusHeight", function(){return(
100
)});
  main.variable(observer("d3")).define("d3", ["require"], function(require){return(
require("d3@6")
)});
  return main;
}

这很棘手,因为函数是可观察的,viewof 返回画笔的视图(最小值和最大值)

Here is a working fiddle



/* new data */
var x = d3.timeDays(new Date(2015, 06, 01), new Date(2020, 10, 30));
var y = Array.from({length: x.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = x.map((v, i) => {
  return {
    "date": v,
    "value": y[i]
  }
});

var margin = {top: 20, right: 20, bottom: 30, left: 40}
var height = 440;
var width = 600;
var focusHeight = 100;

var focusedArea = d3.extent(x);

  const svg = d3.select('#my_chart').append("svg")
      .attr("viewBox", [0, 0, width, height])
      .style("display", "block");

  const clipId = {id: "clip"};

  const clip = svg.append("clipPath")
      .attr("id", clipId.id)
    .append("rect")
      .attr("x", margin.left)
      .attr("y", 0)
      .attr("height", height)
      .attr("width", width - margin.left - margin.right);

  const gx = svg.append("g");

  const gy = svg.append("g");

  const path = svg.append("path")
      .datum(data)
      .attr("clip-path", `url(#${clipId.id})`)
      .attr("fill", "steelblue");

  const updateChart = (focusX, focusY) => {
      gx.call(xAxis, focusX, height);
      gy.call(yAxis, focusY, data.y);
      path.attr("d", area(focusX, focusY));
    };


/* begin observable code */
var focus = () => {
  const svg = d3.select("#focus").append("svg")
      .attr("viewBox", [0, 0, width, focusHeight])
      .style("display", "block");
  const brush = d3.brushX()
      .extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
      .on("brush", brushed)
      .on("end", brushended);

  const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];

  svg.append("g")
      .call(xAxis, x, focusHeight);

  svg.append("path")
      .datum(data)
      .attr("fill", "steelblue")
      .attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));

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

  function brushed({selection}) {
    if (selection) {
      svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
      svg.dispatch("input");
        focusedArea = svg.property('value');
        update();
    }
  }

  function brushended({selection}) {
    if (!selection) {
      gb.call(brush.move, defaultSelection);
    }
  }
  return svg.node();
}

var update = function() {
  const [minX, maxX] = focusedArea;
  const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
  updateChart(x.copy().domain(focusedArea), y.copy().domain([0, maxY]));
}

var area = (x, y) => d3.area()
    .defined(d => !isNaN(d.value))
    .x(d => x(d.date))
    .y0(y(0))
    .y1(d => y(d.value))
    
var x = d3.scaleUtc()
    .domain(d3.extent(data, d => d.date))
    .range([margin.left, width - margin.right])
    
var y = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([height - margin.bottom, margin.top])
    
var xAxis = (g, x, height) => g
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
    
var yAxis = (g, y, title) => g
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y))
    .call(g => g.select(".domain").remove())
    .call(g => g.selectAll(".title").data([title]).join("text")
        .attr("class", "title")
        .attr("x", -margin.left)
        .attr("y", 10)
        .attr("fill", "currentColor")
        .attr("text-anchor", "start")
        .text(title))
        



focus();

下面每个步骤都有一个选项 - 输出在下面的代码片段中,在 jsfiddle 中同样有效。

  1. data的小更新-看评论,主要用value而不是close
  2. 首先引入 focuschart 之外的 Observable 单元格值并包括 width - 我稍后在 update 块中使用了代码一个函数
  3. chart 的主要更改是删除 return 部分并处理从 brush 处理程序调用的 chartUpdate 函数中的 chart 渲染.
  4. focus 不需要是返回 svg.node() 的函数——这是完全有效的,但需要推断它的含义。刷开始和结束日期可以直接传递给 chartUpdate 而无需将值存储在 svg.property 中并使用 dispatch 等广播它(尽管这是 Observable 的一部分尝试演示)
  5. chartUdpatechartreturn 语句中 update 逻辑的简单提升,它接受 [=] 中每个日期范围的缩放23=].

// data
// Changes
// 1. use utcDays because scale is scaleUtc
// 2. rename x and y to xData and yData as x and y are scales
// 3. return 'value' not 'close'
// 4. add the y property for the axis label
const xData = d3.utcDays(new Date(2010, 06, 01), new Date(2020, 10, 30));
var yData = Array.from({length: xData.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = xData.map((v, i) => {
  return {
    "date": v,
    "value": yData[i]
  }
});
Object.assign(data, {y: "↑ Close $"});

// lift from Observable - neither the focus or chart cells
// Changes
// 1. add width as Observable has it as a built-in
// 2. remove the update block
const height = 440;
const focusHeight = 100;
const margin = {top: 20, right: 20, bottom: 30, left: 40}
// add width 
const width = 600;

const xAxis = (g, x, height) => g
  .attr("transform", `translate(0,${height - margin.bottom})`)
  .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));

const yAxis = (g, y, title) => g
  .attr("transform", `translate(${margin.left},0)`)
  .call(d3.axisLeft(y))
  .call(g => g.select(".domain").remove())
  .call(g => g.selectAll(".title").data([title]).join("text")
    .attr("class", "title")
    .attr("x", -margin.left)
    .attr("y", 10)
    .attr("fill", "currentColor")
    .attr("text-anchor", "start")
    .text(title));

const x = d3.scaleUtc()
  .domain(d3.extent(data, d => d.date))
  .range([margin.left, width - margin.right]);

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)])
  .range([height - margin.bottom, margin.top]);
  
const area = (x, y) => d3.area()
  .defined(d => !isNaN(d.value))
  .x(d => x(d.date))
  .y0(y(0))
  .y1(d => y(d.value));
  
// Lift from chart block (first)
// Changes
// 1. need select, append not create
// 2. define a clipPath, discard the use of clipId
// 3. reference the clipPath in chartPth
// 4. rename some vars to specify they are for the chart
// 5. the logic in the return statement deferred to later
const svgChart = d3.select("#viz")
  .append("svg")
  .attr("viewBox", [0, 0, width, height])
  .style("display", "block");
  
// replace clipId and use clippath 
svgChart.append("defs")
  .append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("x", margin.left)
  .attr("width", width - margin.left - margin.right)
  .attr("height", height);
    
const chartGx = svgChart.append("g");
const chartGy = svgChart.append("g");

// refer to the clippath
const chartPath = svgChart.append("path")
  .datum(data)
  .attr("clip-path", "url(#clip)")
  .attr("fill", "steelblue");
      
// lift from focus block (second)
// Changes
// 1. need select, append not create
// 2. change brushed function; don't use dispatch
// 3. svg.node() becomes svg.node().value (as in data 'value')
// 4. chartUpdate is new function with logic from chart cell
const svgFocus = d3.select("#viz")
  .append("svg")
  .attr("viewBox", [0, 0, width, focusHeight])
  .style("display", "block");

const brush = d3.brushX()
  .extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
  .on("brush", brushed)
  .on("end", brushended);
  
const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];

svgFocus.append("g")
  .call(xAxis, x, focusHeight);

svgFocus.append("path")
  .datum(data)
  .attr("fill", "steelblue")
  .attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));

const gb = svgFocus.append("g")
  .call(brush)
  .call(brush.move, defaultSelection);

// update brushed to sync chart with focus
function brushed({selection}) {
  if (selection) {
    // update logic goes here - to get args for chartUpdate
    const [minX, maxX] = selection.map(x.invert, x).map(d3.utcDay.round);
    const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
    const focusX = x.copy().domain([minX, maxX]);
    const focusY = y.copy().domain([0, maxY]);
    
    // call chart update
    chartUpdate(focusX, focusY);
  } 
}

function brushended({selection}) {
  if (!selection) {
    gb.call(brush.move, defaultSelection);
  }
}

// chart  update
function chartUpdate(focusX, focusY) {
  chartGx.call(xAxis, focusX, height);
  chartGy.call(yAxis, focusY, data.y);
  chartPath.attr("d", area(focusX, focusY));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="viz"></div>