{"record":{"id":"c1e960c5399a9a3d","repo":"plotly/plotly.js","slug":"ignored-axout-name-matches-axin-matches-to","errorCode":null,"errorMessage":"ignored <axOut._name>.matches: \"<axIn.matches>\" to avoid an infinite loop","messagePattern":"ignored <axOut\\._name>\\.matches: \"<axIn\\.matches>\" to avoid an infinite loop","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/plots/cartesian/constraints.js","lineNumber":281,"sourceCode":"        }, 'scaleanchor', scaleanchorDflt);\n    }\n\n    if(matches) {\n        axOut._matchGroup = updateConstraintGroups(matchGroups, thisID, matches, 1);\n\n        // Also include match constraints in the scale groups\n        var matchedAx = layoutOut[id2name(matches)];\n        var matchRatio = extent(layoutOut, axOut) / extent(layoutOut, matchedAx);\n        if(isX !== (matches.charAt(0) === 'x')) {\n            // We don't yet know the actual scale ratio of x/y matches constraints,\n            // due to possible automargins, so just leave a placeholder for this:\n            // 'x' means \"x size over y size\", 'y' means the inverse.\n            // in principle in the constraint group you could get multiple of these.\n            matchRatio = (isX ? 'x' : 'y') + matchRatio;\n        }\n        updateConstraintGroups(constraintGroups, thisID, matches, matchRatio);\n    } else if(axIn.matches && axIds.indexOf(axIn.matches) !== -1) {\n        Lib.warn('ignored ' + axOut._name + '.matches: \"' +\n            axIn.matches + '\" to avoid an infinite loop');\n    }\n\n    if(scaleanchor) {\n        var scaleratio = coerce('scaleratio');\n\n        // TODO: I suppose I could do attribute.min: Number.MIN_VALUE to avoid zero,\n        // but that seems hacky. Better way to say \"must be a positive number\"?\n        // Of course if you use several super-tiny values you could eventually\n        // force a product of these to zero and all hell would break loose...\n        // Likewise with super-huge values.\n        if(!scaleratio) scaleratio = axOut.scaleratio = 1;\n\n        updateConstraintGroups(constraintGroups, thisID, scaleanchor, scaleratio);\n    } else if(axIn.scaleanchor && axIds.indexOf(axIn.scaleanchor) !== -1) {\n        Lib.warn('ignored ' + axOut._name + '.scaleanchor: \"' +\n            axIn.scaleanchor + '\" to avoid either an infinite loop ' +\n            'and possibly inconsistent scaleratios, or because this axis ' +","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/plotly/plotly.js/blob/1d090e0b5ffb8d0fdf6e0e4ff51d3f3cf67f1314/src/plots/cartesian/constraints.js#L263-L299","documentation":"When axis axOut declares matches pointing at another axis, plotly.js ignores the setting if it would create a cycle (the matched axis is part of the same group/loop) — matches is processed earlier in the chain, so honoring it would loop forever. The setting is dropped and this warning explains why. RAISED IN: handleOneAxDefaults, i.e. during layout default computation.","triggerScenarios":"Setting layout.xaxis2.matches = 'x' where x itself already matches x2 (or is linked into the same group via matches/scaleanchor chains), creating a circular matches reference among axes.","commonSituations":"Copy-pasting axis configs and duplicating matches on both axes; programmatically linking subplot axes in a loop (x2 matches x3, x3 matches x2); templates that apply matches broadly.","solutions":["Break the cycle: keep matches on only one side of the pair (remove matches from the axis that gets the warning).","Define a chain direction (x2 matches x, x3 matches x2) rather than mutual references.","Restructure so all axes in the synchronized set match a single base axis."],"exampleFix":"// before\nlayout.xaxis2 = {matches: 'x'};\nlayout.xaxis = {matches: 'x2'}; // cycle\n// after\nlayout.xaxis2 = {matches: 'x'};\n// remove matches from xaxis","handlingStrategy":"validation","validationCode":"function assertAcyclicMatches(layout) {\n  const seen = new Set();\n  const visit = (ax, chain) => {\n    if (chain.includes(ax)) throw new Error(`cyclic matches: ${chain.join(' -> ')} -> ${ax}`);\n    const m = (layout[ax] || {}).matches;\n    if (m) visit(m, [...chain, ax]);\n  };\n  Object.keys(layout).filter(k => k.endsWith('axis')).forEach(ax => visit(ax, []));\n}\nassertAcyclicMatches(layout);","typeGuard":"const isAcyclicMatches = (layout, ax) => { const s = new Set(); let cur = (layout[ax] || {}).matches; while (cur) { if (s.has(cur)) return false; s.add(cur); cur = (layout[cur] || {}).matches; } return true; };","tryCatchPattern":null,"preventionTips":["Set matches in one direction only (child matches base).","Never set matches on both axes of a pair.","Draw the matches graph when linking many subplots.","Keep a single base axis for all synchronized axes."],"tags":["constraints","axes","cyclic-reference"],"backgroundTag":"cyclic-axis-reference","analyzedSha":"1d090e0b5ffb8d0fdf6e0e4ff51d3f3cf67f1314","analyzedAt":"2026-09-02T22:03:39.906Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}