See how policy choices change what you pay (and what the insurer pays)
pet insurance
data visualization
Published
September 10, 2025
How does pet insurance actually play out when the vet bills show up? This interactive tool helps you see who pays what — how much comes out of your pocket and how much gets covered by insurance.
Adjust the values related to the policy parameters, set the estimated annual vet expense and covered vet expense and see in real time how your policy choices affect the bill split, so you can understand the value-add of the pet insurance policy and make more informed decisions about your coverage options.
Definitions
Monthly Premium $
The payment you make to the insurer every month in order to keep your policy active.
Reimbursement Level %
The insurer’s share of the covered vet medical expenses expressed as a percentage.
Annual Deductible $
This is the amount you must first pay before the insurer will begin paying their reimbursement. Some insurers apply the deductible to the reimbursement amount, others apply it to the covered amount. This tool currently works for the former category of insurers. If you would like support for the latter kind, send me a message. This amount resets every year.
Annual Total Vet Medical Expense $
Total vet medical expenses for a year. This can be an estimate for next year, or based on data collected from the previous year, or based on a specific kind of emergency the cost of which you have an estimate for that you are trying this visualization for.
Annual Total Vet Medical Expense Not Covered By Insurance $
The portion, in dollars, of the total annual vet medical expenses that is not covered by the insurer. Preventative care expenses, elective procedures, vet exam fees, cost of shipping of diagnostic tests to labs, pre-existing conditions, etc. are some examples of expenses that might be excluded, i.e. not covered, by some insurers.
data = {const links =awaitFileAttachment("data/links.csv").csv({typed:true});const nodes =awaitFileAttachment("data/nodes.csv").csv({typed:true});return {nodes, links};}
not_covered = (() => {if (annual_premium <1) {// There is no insurerreturn policy_parameters.invoice; } elseif (policy_parameters.invoice<1) {// There are no vet expensesreturn0; } elseif (policy_parameters.invoice_not_covered> policy_parameters.invoice) {// not covered expenses cannot be greater than total expensesreturn policy_parameters.invoice; } else {return policy_parameters.invoice_not_covered; }})()
covered = policy_parameters.invoice- not_covered
deductible = (() => {if (annual_premium <1) {// There is no insurerreturn0; } elseif (policy_parameters.invoice<1) {// There are no vet expensesreturn0; } elseif (covered * policy_parameters.reimbursement/100< policy_parameters.deductible) {// Vet expenses to be reimbursed are less than the deductiblereturn covered * policy_parameters.reimbursement/100; } else {return policy_parameters.deductible; }})()
co_insurance = (() => {if (annual_premium <1) {// There is no insurerreturn0; } elseif (policy_parameters.invoice<1) {// There are no vet expensesreturn0; } else {return covered * (1- policy_parameters.reimbursement/100); }})()
reimbursement = (() => {if (annual_premium <1) {// There is no insurerreturn0; } elseif (policy_parameters.invoice<1) {// There are no vet expensesreturn0; } else {return covered * policy_parameters.reimbursement/100- deductible; }})()
d3 =require("d3@7","d3-sankey@0.12")//d3Sankey = require.alias({"d3-array": d3, "d3-shape": d3, "d3-sankey": "d3-sankey@0.12.3/dist/d3-sankey.min.js"})("d3-sankey")chart = {// Specify the dimensions of the chart.const width =928;const height =600;const format = d3.format(",.2f")// Create an SVG container.const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0,0, width, height])//.attr("style", "max-width: 100%; height: auto; font: 20px sans-serif;");.attr("style","max-width: 100%; height: auto; font: 24px Garamond;");// Constructs and configures a Sankey generator.const sankey = d3.sankey().nodeId(d => d.name)// d3.sankeyLeft, d3.sankeyRight, d3.sankeyCenter, d3.sankeyJustify, d3[nodeAlign].nodeAlign(d3.sankeyLeft) .nodeWidth(15).nodePadding(10).extent([[1,5], [width -1, height -5]]);// Changes data to reflect the values in the input selections// premium -> reimbursement data.links[0].value= annual_premium;//12 * policy_parameters.premium_monthly// deductible -> medical-expense data.links[1].value= deductible;//policy_parameters.deductible // coinsurance -> medical-expense data.links[2].value= co_insurance;// (policy_parameters.invoice - policy_parameters.invoice_not_covered) * (1 - policy_parameters.reimbursement/100) // not-covered -> medical-expense data.links[3].value= not_covered;//policy_parameters.invoice_not_covered // reimbursement -> medical-expense data.links[4].value= reimbursement;//(policy_parameters.invoice - policy_parameters.invoice_not_covered) * policy_parameters.reimbursement/100 - policy_parameters.deductible // Applies it to the data. We make a copy of the nodes and links objects// so as to avoid mutating the original.const {nodes, links} =sankey({nodes: data.nodes.map(d =>Object.assign({}, d)),links: data.links.map(d =>Object.assign({}, d)) });// Defines a color scale.// options: d3.schemeCategory10 d3.schemeTableau10const color = d3.scaleOrdinal(d3.schemeTableau10);// Creates the rects that represent the nodes.const rect = svg.append("g").attr("stroke","#000").selectAll().data(nodes).join("rect").attr("x", d => d.x0).attr("y", d => d.y0).attr("height", d => d.y1- d.y0).attr("width", d => d.x1- d.x0).attr("fill", d =>color(d.category));// Adds a title on the nodes. rect.append("title").text(d =>`${d.name}\n${format(d.value)}`);// Creates the paths that represent the links.const link = svg.append("g").attr("fill","none").attr("stroke-opacity",0.5).selectAll().data(links).join("g").style("mix-blend-mode","multiply");// Creates a gradient, if necessary, for the source-target color option.const linkColor ="source";if (linkColor ==="source-target") {const gradient = link.append("linearGradient").attr("id", d => (d.uid= DOM.uid("link")).id).attr("gradientUnits","userSpaceOnUse").attr("x1", d => d.source.x1).attr("x2", d => d.target.x0); gradient.append("stop").attr("offset","0%").attr("stop-color", d =>color(d.source.category)); gradient.append("stop").attr("offset","100%").attr("stop-color", d =>color(d.target.category)); } link.append("path").attr("d", d3.sankeyLinkHorizontal()).attr("stroke", linkColor ==="source-target"? (d) => d.uid: linkColor ==="source"? (d) =>color(d.source.category): linkColor ==="target"? (d) =>color(d.target.category) : linkColor).attr("stroke-width", d =>Math.max(1, d.width)); link.append("title").text(d =>`${d.source.name} → ${d.target.name}\n\$${format(d.value)}`);// Adds labels on the nodes. svg.append("g").selectAll().data(nodes).join("text").attr("x", d => d.x0< width /2? d.x1+6: d.x0-6).attr("y", d => (d.y1+ d.y0) /2).attr("dy","0.35em").attr("text-anchor", d => d.x0< width /2?"start":"end").text(d => d.name);return svg.node();}
Parameters
viewof policy_parameters = Inputs.form({premium_monthly: Inputs.range([0,2000], {label:"Monthly Premium $",step:0.01,value:100}),reimbursement: Inputs.range([0,100], {label:"Reimbursement Level %",step:10,value:90}),deductible: Inputs.radio([100,250,500,750,1000,1500,2000], {label:"Annual Deductible $",value:250}),invoice: Inputs.range([0,60000], {label:"Annual Total Vet Medical Expense $",step:0.01,value:10000}),invoice_not_covered: Inputs.range([0,60000], {label:"Annual Total Vet Medical Expense Not Covered By Insurance $",step:0.01,value:1500}),notes: Inputs.text({label:"Your Notes",value:""})//deductible_stage: Inputs.select(["After", "Before"], {label: "Deductible withdrawn after or before calculating reimbursement amount", value: "After"}),//option7: Inputs.checkbox(["Before", "After"], {label: "Select boxes", value: "After"})//})
Expense Sharing Tabulated
category
from
to
amount ($)
premium
you
insurer
deductible
you
vet
co-insurance
you
vet
not-covered
you
vet
reimbursement
insurer
vet
Thoughts on this visualizer, want to request additional functionality or report an issue or inaccuracy? Reply by email.