Explore how policy choices affect what you pay (and what the insurer earns vs. pays)
pet insurance
data visualization
Published
September 10, 2025
Modified
September 14, 2025
How does pet insurance actually play out when the vet bills show up? This interactive tool helps you see the full picture - how much of the vet bill you pay, how much the insurer pays, and the cost to you for the insurance protection.
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 these numbers, so you can better 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.
Maximum Benefit $
This is the maximum amount the insurer will reimburse. This cap may apply in different ways - a lifetime maximum (total paid over the pet’s life), an annual maximum (total paid in one year), or a per-condition maximum (total paid for a specific condition).
Reimbursement Level %
The insurer’s share of the covered vet medical expenses expressed as a percentage. The complementary percentage (100 - Reimbursement Level) is referred to as the co-insurance percentage.
Annual Deductible $
This is the amount you must first pay before the insurer will begin paying their reimbursement. This amount resets every year.
Deductible withdrawn before or after calculating co-insurance
Some insurers apply the deductible to the covered amount i.e. before calculating the co-insurance amount. Others apply the deductible after calculating the co-insurance amount. The latter is more common among pet insurers (hence the default for this tool), though there are more than a couple insurers that calculate it according to the former method.
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.
Parameters
viewof policy_parameters = Inputs.form({premium_monthly: Inputs.range([0,5000], {label:"Monthly Premium $",step:0.01,value:100}),limit: Inputs.select(["Unlimited",20000,10000,7500,5000,3500,2000], {label:"Maximum Benefit $",value:"Unlimited"}),reimbursement: Inputs.range([0,100], {label:"Reimbursement Level %",step:10,value:90}),deductible: Inputs.radio([100,200,250,500,700,750,1000,1500,2000,3000,4000,5000], {label:"Annual Deductible $",value:250}),deductible_stage: Inputs.radio(["before","after"], {label:"Deductible withdrawn before or after calculating co-insurance",value:"after"}),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 Note",value:"",placeholder:""}),//option7: Inputs.checkbox(["Before", "After"], {label: "Select boxes", value: "After"})//})
ns = Inputs.range().classList[0]html`<style>.${ns} div label { background-color: #f4f4f4;}.${ns} div label:hover,.${ns} div label:active,.${ns} div label:focus { background-color: #fefefe;}.${ns} div input { accent-color: #888}</style>`
Expense Sharing Tabulated
from
to
category
amount ($)
you
insurer
premium
you
vet
deductible
you
vet
co-insurance
you
vet
not-covered
you
vet
max-benefit-spillover
insurer
vet
reimbursement
data = {const links =awaitFileAttachment("data/links.csv").csv({typed:true});const nodes =awaitFileAttachment("data/nodes.csv").csv({typed:true});return {nodes, links};}
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: 20px 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.sankeyJustify) .nodeSort(null) //(inputOrder ? null : undefined).nodeWidth(15).nodePadding(10).extent([[1,5], [width -1, height -15]]);// Changes data to reflect the values in the input selections// premium -> reimbursement data.links[0].value= calculated_policy_parameters.annual_premium// deductible -> medical-expense data.links[1].value= calculated_policy_parameters.deductible// coinsurance -> medical-expense data.links[2].value= calculated_policy_parameters.co_insurance// not-covered -> medical-expense data.links[3].value= calculated_policy_parameters.not_covered// limit-spillover -> medical-expense data.links[4].value= calculated_policy_parameters.limit_spillover// reimbursement -> medical-expense data.links[5].value= calculated_policy_parameters.reimbursement// 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","#d4d4d4") //#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 ="#d4d4d4";//"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();}
Thoughts on this visualizer, want to request additional functionality or report an issue? Reply by email.