lost notebook

I can’t seem to be allowed to retrieve https://beta.observablehq.com/d/2b4e53dd7f3aa7b8 for some Ooops reason. (Maybe my fault, I don’t remember what I did on this notebook)

The last full backup I have of this notebook is version 1084. After that, my exports are still saying “version 1084” (so I suppose I have not edited it), but the contents retrieved is error:500.

{
  "id": "2b4e53dd7f3aa7b8",
  "version": 1084,
  "slug": null,
  "thumbnail": "7513365cdf3bb653120cfadbba8b89c9254f83e4ce371f0ad7a29851fa82d7ec",
  "title": "Voronoi projection — cube + 8",
  "update_time": "2018-06-14T19:09:02.755Z",
  "likes": 0,
  "fork_of": {
    "id": "8e3fdfca17fe163f",
    "version": 914
  },
  "code": {
    "id": "2b4e53dd7f3aa7b8",
    "slug": null,
    "trashed": false,
    "likes": 0,
    "forks": 0,
    "fork_of": null,
    "creator": {
      "id": "45a379fcfcb14253",
      "github_id": "7001",
      "avatar_url": "https://avatars0.githubusercontent.com/u/7001?v=4",
      "login": "fil",
      "name": "Fil",
      "bio": "Vocateur; journalism, code and maps\r\n↬ portfolio: https://illisible.net/philippe-riviere",
      "home_url": "https://visionscarto.net/"
    },
    "team": null,
    "collections": [],
    "publish_time": null,
    "publish_version": null,
    "access_key": "657655b8ab4016bf",
    "version": 1084,
    "title": "Voronoi projection — cube + 8",
    "nodes": [
      {
        "id": 0,
        "value": "md`# Voronoi projection — cube + 8`",
        "pinned": false
      },
      {
        "id": 15,
        "value": "VORONOI = map(projection)",
        "pinned": false
      },
      {
        "id": 862,
        "value": "md`Points 0 to 5 are the cube's faces centers. We add more points on the ±${parallel}° parallels:`",
        "pinned": false
      },
      {
        "id": 1045,
        "value": "viewof addpoints = md`<input type=checkbox>`",
        "pinned": false
      },
      {
        "id": 972,
        "value": "parallel = 19.5",
        "pinned": false
      },
      {
        "id": 876,
        "value": "list = addpoints\n  ? cube.concat([ [45,parallel], [135,parallel], [-45,parallel], [-135,parallel] ]).concat([ [45,-parallel], [135,-parallel], [-45,-parallel], [-135,-parallel] ])\n  : cube",
        "pinned": true
      },
      {
        "id": 945,
        "value": "cube = [[0,90], [0,-90], [0,0], [90,0], [-180,0], [-90,0]]",
        "pinned": false
      },
      {
        "id": 825,
        "value": "points = ({\n    type: \"FeatureCollection\",\n    features: d3.range(list.length).map(i => {\n      var f = list[i];\n      f[0] += 0.01 * i;\n      return {\n          type: \"Point\",\n          index: i,\n          properties: { land: ((i == 6) || d3.geoContains(land, f)) ? 0 : 100000 },\n          coordinates: f\n      }\n    })\n })",
        "pinned": false
      },
      {
        "id": 771,
        "value": "v = d3.geoVoronoi()(points)",
        "pinned": false
      },
      {
        "id": 779,
        "value": "polygons = v.polygons()",
        "pinned": false
      },
      {
        "id": 752,
        "value": "projection = d3.geoPolyhedralVoronoi(net, polygons)\n  //.angle(29)\n  .fitExtent([[0,0],[950,500]], {type:\"Sphere\"})",
        "pinned": false
      },
      {
        "id": 774,
        "value": "net = addpoints ? [-1,10,6,11,11,12,0,11,12,13,6,1,1,1] : [-1,3,1,0,1,1]",
        "pinned": false
      },
      {
        "id": 513,
        "value": "md`-----------\n_Play zone_`",
        "pinned": false
      },
      {
        "id": 569,
        "value": "viewof show_structure = md`<input type=checkbox checked>`",
        "pinned": false
      },
      {
        "id": 869,
        "value": "viewof show_face_centroid = md`<input type=checkbox>`",
        "pinned": false
      },
      {
        "id": 622,
        "value": "viewof show_sphere = md`<input type=checkbox checked>`",
        "pinned": false
      },
      {
        "id": 630,
        "value": "viewof show_land = md`<input type=checkbox checked>`",
        "pinned": false
      },
      {
        "id": 660,
        "value": "viewof show_equator = md`<input type=checkbox>`\n",
        "pinned": false
      },
      {
        "id": 11,
        "value": "function map(projection) {\n  const context = DOM.context2d(width, width / 2);\n\n  projection.fitExtent([[2,2],[width-2, width/2-2]], {type:\"Sphere\"});\n  \n  var path = d3.geoPath(projection, context);\n  context.clearRect(0,0,width, width/2);\n  if (show_sphere)\n    context.beginPath(), path({type:\"Sphere\"}), context.fillStyle = \"#fefef6\", context.fill();\n  if (show_sphere)\n    context.beginPath(), path(d3.geoGraticule()()), context.strokeStyle = \"#ccc\", context.stroke();\n  if (show_equator)\n    context.beginPath(),\n      path(d3.geoGraticule().step([0,100]).extent([[-179.99, -25], [179.99, 25]])()),\n      context.strokeStyle = \"brown\", context.stroke();\n  if (show_land)\n    context.beginPath(), path(land), context.fillStyle = \"black\", context.fill();\n  if (show_sphere || show_structure || !show_land)\n    context.beginPath(), path({type: \"Sphere\"}), context.strokeStyle = \"#000\", context.stroke();\n  \n  // Polyhedral projections expose their structure as projection.tree()\n  // To draw them we need to cancel the rotate\n  if (show_structure) {\n    var rotate = projection.rotate();\n    projection.rotate([0,0,0]);\n\n    // run the tree of faces to get all sites and folds\n    var sites = [], folds = [], i = 0;\n\n    function recurse(face) {\n      var site = (!show_face_centroid) ? face.site : d3.geoCentroid({type:\"Polygon\", coordinates:[face.face]});\n      site.id = face.id || i++;\n      sites.push(site);\n      if (face.children) {\n        face.children.forEach(function(child) {\n          folds.push({\n            type:\"LineString\",\n            coordinates: child.shared.map(\n              e => d3.geoInterpolate(e, face.centroid)(1e-5)\n            )\n          });\n          recurse(child);\n        });\n      }\n    }\n    recurse(projection.tree());\n\n    // sites & numbers\n    context.beginPath(),\n      path.pointRadius(10)({type:\"MultiPoint\", coordinates: sites}),\n      context.fillStyle = \"white\", context.strokeStyle = \"black\",\n      context.fill(), context.stroke();\n    sites.forEach(site => {\n      context.textAlign = \"center\", context.fillStyle = \"black\",\n        context.font = \"16px Georgia\", context.textBaseline = \"middle\",\n        context.fillText(site.id, projection(site)[0], projection(site)[1] - 1);\n    });\n  \n    // folding lines\n    folds.forEach(fold => {\n      context.beginPath(),\n        context.lineWidth = 1, context.setLineDash([3,4]), context.strokeStyle = \"#a55\",\n        path(fold), context.stroke(), context.setLineDash([]);\n    });\n\n    // restore the projection’s rotate\n    projection.rotate(rotate);\n  }\n\n  return context.canvas;\n}",
        "pinned": false
      },
      {
        "id": 9,
        "value": "land = {\n  const topojson = await require(\"topojson\");\n  const world = await d3.json(\"https://unpkg.com/world-atlas@1.1.4/world/110m.json\");\n  return topojson.feature(world, world.objects.land);\n}",
        "pinned": false
      },
      {
        "id": 2,
        "value": "d3 = require(\"d3-array\", \"d3-fetch\", \"d3-geo\", \"d3-geo-polygon\", \"d3-geo-voronoi\") //\"d3-geo-polygon\"",
        "pinned": false
      },
      {
        "id": 756,
        "value": "kruskal = {\n  // https://github.com/mikolalysenko/union-find\nconst UnionFind = (function() {\n\n\"use strict\"; \"use restrict\";\n\n\nfunction UnionFind(count) {\n  this.roots = new Array(count);\n  this.ranks = new Array(count);\n  \n  for(var i=0; i<count; ++i) {\n    this.roots[i] = i;\n    this.ranks[i] = 0;\n  }\n}\n\nvar proto = UnionFind.prototype\n\nObject.defineProperty(proto, \"length\", {\n  \"get\": function() {\n    return this.roots.length\n  }\n})\n\nproto.makeSet = function() {\n  var n = this.roots.length;\n  this.roots.push(n);\n  this.ranks.push(0);\n  return n;\n}\n\nproto.find = function(x) {\n  var x0 = x\n  var roots = this.roots;\n  while(roots[x] !== x) {\n    x = roots[x]\n  }\n  while(roots[x0] !== x) {\n    var y = roots[x0]\n    roots[x0] = x\n    x0 = y\n  }\n  return x;\n}\n\nproto.link = function(x, y) {\n  var xr = this.find(x)\n    , yr = this.find(y);\n  if(xr === yr) {\n    return;\n  }\n  var ranks = this.ranks\n    , roots = this.roots\n    , xd    = ranks[xr]\n    , yd    = ranks[yr];\n  if(xd < yd) {\n    roots[xr] = yr;\n  } else if(yd < xd) {\n    roots[yr] = xr;\n  } else {\n    roots[yr] = xr;\n    ++ranks[xr];\n  }\n}\n\n\treturn UnionFind;\n})()\n\nfunction kruskal(graph, dist) {\n// 1   A := ø\n\tconst A = [];\n// 2   pour chaque sommet v de G :\n// 3      créerEnsemble(v)\n  let n = -Infinity;\n  graph.forEach(l => {\n    if (l.source.index > n) n = l.source.index;\n    if (l.target.index > n) n = l.target.index;\n  })\n  const uf = new UnionFind(n);\n// 4   trier les arêtes de G par poids croissant\n\tgraph = graph.map(l => {\n\t\tl.w = l.w || l.length || dist(l.source, l.target);\n\t\treturn l;\n\t})\n  graph.sort((a,b) => d3.ascending(a.w, b.w))\n// 5   pour chaque arête (u, v) de G prise par poids croissant :\n  .forEach(l => {\n// 6      si find(u) ≠ find(v) :\n\t\tif (uf.find(l.source.index) != uf.find(l.target.index)) {\n// 7         ajouter l'arête (u, v) à l'ensemble A\n\t\t\tA.push(l);\n// 8         union(u, v)\n\t\t\tuf.link(l.source.index, l.target.index);\n\t\t}\n\t});\n// 9   retourner A\n\treturn A;\n//\tyield uf;\n}\n  \n  return kruskal;\n}",
        "pinned": false
      }
    ]
  }
}

might be a case of unable to open draft notebook

I’ve deployed a fix, and your notebooks should be working again! Sorry for the trouble. Yes, this was a variation of the same bug regarding the interaction of our old implementation of revert and forking your own notebooks.

To give a little background:

Prior to last week revert was destructive: it simply deleted the events to revert. Now revert is nondestructive, just like normal editing: we record a revert event to rewind to an earlier version.

Nondestructive editing provides a useful property: a given notebook at a given version always contains exactly the same content. That was not true in the past due solely to destructive revert. For example, the version 914 of notebook A at the time you forked notebook B looks different from the new version 914 of notebook A after a revert and subsequent edits.

When we made revert nondestructive, we also optimized forks: rather than create a copy of the notebook you are forking, the new fork retains only a reference (pointer) to the parent notebook at the fork version. This optimization relies on nondestructive editing because it requires that the meaning of this reference not change over time.

Our mistake was that we assumed revert was limited enough in scope that forked versions never changed, and thus we could retroactively apply this optimization to past forks. However, that was not true in the past if you forked your own notebook (and then subsequently reverted the parent notebook). The fix disables the optimization when we detect the forked version was reverted.

4 Likes

Thanks a lot for the quick recovery, and even better for the very detailed explanation!

2 Likes

I quite often play with existing notebooks and if the changes seem promising, I fork them and revert the notebook to its previous shared or published status. This is probably why what happened in this case too.

thanks for sharing this insight on how forks are managed by Observable.

do you think it would also be beneficial to have this feature related to forks and notebook updates?

We’re planning a history feature to show how a notebook has changed over time. Authors will be able to browse every version, while readers will only be able to see published versions. Authors will also be able to restore work, say by reverting to an earlier version or cherry-picking changes. And forking, too, of course.

For this to work well, I expect we’ll want a way of attaching notes to specific versions (the equivalent of git tags or commit messages), since the number of automatically-saved versions quickly becomes unmanageable. But version notes should likely not be limited to publishing, since you may want this ability for private notebooks, and you may want to tag versions prior to publishing.

If you update a published notebook in a meaningful way, a good journalistic practice is to add a correction note to the notebook itself, either at the top or the bottom depending on how significant the correction is.

3 Likes

.@mbostock this is great! Could you move it to the publish commit comments thread I started?

my bad for encroaching on lost notebook that was resolved.

I just wanted to get your eyes on Publish commits comments since it impedes daily notebook updates and I thought it was related.

I reformulated that feature request and appreciate your extensive response on versioning and tagging you guys have in mind for the next rev. in your product dev pipeline!