Skip to content

caldwellc/d3-brush-snapping

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

d3-brush-snapping

Library for adding snapping to a d3 brush.

npm version

Example

See it in action here

Snapping Example

Examples

<!DOCTYPE html>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-brush-tooltip@1.0.1"></script>
<script src="https://cdn.jsdelivr.net/npm/humanize-duration@3.32.1"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-brush-snapping@1.0.0"></script>
<link rel="stylesheet" href="css/style.css">
<html>
    <body>
        <h2>Linear Chart with Snapping</h2>
        <div id="chart-div">
        </div>
        <script>
            {
                // create chart
                let width = 600;
                let height = 400;

                const margin = { top: 10, right: 30, bottom: 30, left: 40 };
                width = width - margin.left - margin.right,
                height = height - margin.top - margin.bottom;

                // append the svg object to the body of the page
                var svg = d3.select("#chart-div")
                    .append("svg")
                    .attr("width", width + margin.left + margin.right)
                    .attr("height", height + margin.top + margin.bottom)
                    .append("g")
                    .attr("transform",
                        "translate(" + margin.left + "," + margin.top + ")");

                const maxX = 1000;

                // add x axis
                const x = d3.scaleLinear()
                    .domain([0, maxX])
                    .range([0, width]);

                svg.append("g")
                    .attr("transform", "translate(0," + height + ")")
                    .call(d3.axisBottom(x));

                // generate random data
                const data = [];
                for (let i = 0; i < 1000; i++) {
                    data.push(Math.random() * maxX);
                }

                // create bins
                const bin = d3.bin()
                    .domain(x.domain())
                    .thresholds(x.ticks(20));

                const bins = bin(data);

                // add y axis
                const y = d3.scaleLinear()
                    .domain([0, d3.max(bins, function (d) { return d.length; })])
                    .range([height, 0]);

                svg.append("g")
                    .call(d3.axisLeft(y));

                // append the bar rectangles to the svg element
                svg.selectAll("rect")
                    .data(bins)
                    .enter()
                    .append("rect")
                    .attr("x", 1)
                    .attr("transform", function (d) { return "translate(" + x(d.x0) + "," + y(d.length) + ")"; })
                    .attr("width", function (d) { return Math.max(0, x(d.x1) - x(d.x0) - 1); })
                    .attr("height", function (d) { return height - y(d.length); })
                    .style("fill", "#69b3a2")

                // add a brush container to the chart
                const brushContainer = svg.append("g");

                // create the brush
                const brush = d3.brushX().extent([[0, 0], [width, height]]);
                brushContainer.call(brush);

                // move brush to the initial position
                const initialSelection = [x(100), x(200)];
                brushContainer.call(brush.move, initialSelection);
                const snappingThreshold = 50;
                const minX = 0;

                // add linear snapping to brush
                window.d3BrushSnapping.addLinearSnappingToBrush(brush, brushContainer, x, snappingThreshold, minX, maxX);
            }
        </script>
        <h2>Date Range Chart with Snapping</h2>
        <div id="time-chart-div">
        </div>
        <script>
            {
                // create chart
                let width = 600;
                let height = 400;

                const margin = { top: 10, right: 30, bottom: 30, left: 40 };
                width = width - margin.left - margin.right,
                height = height - margin.top - margin.bottom;

                // append the svg object to the body of the page
                var svg = d3.select("#time-chart-div")
                    .append("svg")
                    .attr("width", width + margin.left + margin.right)
                    .attr("height", height + margin.top + margin.bottom)
                    .append("g")
                    .attr("transform",
                        "translate(" + margin.left + "," + margin.top + ")");

                const maxX = 1000;

                const startDate = new Date("2000-01-01");
                const endDate = new Date("2000-01-02");
                const brushStart = new Date("2000-01-01:06:00:00");
                const brushEnd = new Date("2000-01-01:09:00:00");

                // add x axis
                const x = d3.scaleUtc([startDate, endDate], [0, width]);


                svg.append("g")
                    .attr("transform", "translate(0," + height + ")")
                    .call(d3.axisBottom(x));

                // generate random data
                const data = [];
                for (let i = 0; i < 1000; i++) {
                    const hour = Math.round(Math.random() * 24);
                    data.push(new Date(Date.UTC(2000, 0, 1, hour, 0, 0, 0)));
                }

                // create bins
                const bin = d3.bin()
                    .domain(x.domain())
                    .thresholds(x.ticks(20));

                const bins = bin(data);

                // add y axis
                const y = d3.scaleLinear()
                    .domain([0, d3.max(bins, function (d) { return d.length; })])
                    .range([height, 0]);

                svg.append("g")
                    .call(d3.axisLeft(y));

                // append the bar rectangles to the svg element
                svg.selectAll("rect")
                    .data(bins)
                    .enter()
                    .append("rect")
                    .attr("x", 1)
                    .attr("transform", function (d) { return "translate(" + x(d.x0) + "," + y(d.length) + ")"; })
                    .attr("width", function (d) { return Math.max(0, x(d.x1) - x(d.x0) - 1); })
                    .attr("height", function (d) { return height - y(d.length); })
                    .style("fill", "#00619E")

                // add a brush container to the chart
                const brushContainer = svg.append("g");

                // create the brush
                const brush = d3.brushX().extent([[0, 0], [width, height]]);
                brushContainer.call(brush);

                // move brush to the initial position
                const initialSelection = [x(brushStart), x(brushEnd)];
                brushContainer.call(brush.move, initialSelection);

                // add time snapping to brush
                window.d3BrushSnapping.addTimeSnappingToBrush(brush, brushContainer, {
                    timeScale: x,
                    numTicks: 24,
                    extentStartMs: startDate.getTime(),
                    extentEndMs: endDate.getTime(),
                    brushStartMs: brushStart.getTime(),
                    brushEndMs: brushEnd.getTime(),
                    width: width,
                });

            }
        </script>
        <h2>Date Range Chart with Snapping (Extent that doesn't align with ticks)</h2>
        <div id="irregular-time-chart-div">
            <div id="tooltip" class="tooltip-default"></div>
        </div>
        <script>
            {
                // create chart
                let width = 600;
                let height = 400;

                const margin = { top: 10, right: 30, bottom: 30, left: 40 };
                width = width - margin.left - margin.right,
                height = height - margin.top - margin.bottom;

                // append the svg object to the body of the page
                var svg = d3.select("#irregular-time-chart-div")
                    .append("svg")
                    .attr("width", width + margin.left + margin.right)
                    .attr("height", height + margin.top + margin.bottom)
                    .append("g")
                    .attr("transform",
                        "translate(" + margin.left + "," + margin.top + ")");

                const maxX = 1000;

                const startDate = new Date("1999-12-31T23:25:59.992");
                const endDate = new Date("2000-01-02");
                const brushStart = new Date("2000-01-01T06:00:00");
                const brushEnd = new Date("2000-01-01T09:00:00");

                // add x axis
                const x = d3.scaleUtc()
                    .domain([startDate, endDate])
                    .clamp(true)
                    .rangeRound([0, width]);
                

                svg.append("g")
                    .attr("transform", "translate(0," + height + ")")
                    .call(d3.axisBottom(x));

                // generate random data
                const data = [];
                for (let i = 0; i < 1000; i++) {
                    const hour = Math.round(Math.random() * 24);
                    data.push(new Date(Date.UTC(2000, 0, 1, hour, 0, 0, 0)));
                }

                // create bins
                const bin = d3.bin()
                    .domain(x.domain())
                    .thresholds(x.ticks(20));

                const bins = bin(data);

                // add y axis
                const y = d3.scaleLinear()
                    .domain([0, d3.max(bins, function (d) { return d.length; })])
                    .range([height, 0]);

                svg.append("g")
                    .call(d3.axisLeft(y));

                // append the bar rectangles to the svg element
                svg.selectAll("rect")
                    .data(bins)
                    .enter()
                    .append("rect")
                    .attr("x", 1)
                    .attr("transform", function (d) { return "translate(" + x(d.x0) + "," + y(d.length) + ")"; })
                    .attr("width", function (d) { return Math.max(0, x(d.x1) - x(d.x0) - 1); })
                    .attr("height", function (d) { return height - y(d.length); })
                    .style("fill", "#00619E")

                // add a brush container to the chart
                const brushContainer = svg.append("g");

                // create the brush
                const brush = d3.brushX().extent([[0, 0], [width, height]]);
                brushContainer.call(brush);

                // move brush to the initial position
                const initialSelection = [x(brushStart), x(brushEnd)];
                brushContainer.call(brush.move, initialSelection);

                let previousResult = {
                    pixels: initialSelection,
                    values: [brushStart.getTime(), brushEnd.getTime()],
                };

                // onSelectionChange, track the snapped result
                const onSelectionChanged = (event, snappedResult) => {
                    previousResult.pixels = snappedResult.pixels;
                    previousResult.values = snappedResult.values;
                };

                // add time snapping to brush
                window.d3BrushSnapping.addTimeSnappingToBrush(brush, brushContainer, {
                    timeScale: x,
                    numTicks: 24,
                    extentStartMs: startDate.getTime(),
                    extentEndMs: endDate.getTime(),
                    brushStartMs: brushStart.getTime(),
                    brushEndMs: brushEnd.getTime(),
                    width: width,
                    allowExtentSnapping: true,
                }, onSelectionChanged);

                const initialText = humanizeDuration(Math.abs(previousResult.values[1] - previousResult.values[0]), { units: ["w", "d", "h", "m", "s", "ms"], round: true });

                const tooltipId = "#tooltip";
                // add tooltip to the brush to view time duration
                window.d3BrushTooltip.addTooltipToBrush(tooltipId, brush, brushContainer, ({ selection }) => {
                    let humanReadableDuration = null;
                    if (!selection) {
                        humanReadableDuration = initialText;
                    } else if (selection[0] === selection[1]) {
                        humanReadableDuration = null;
                    } else if (previousResult != null) {
                        humanReadableDuration = humanizeDuration(Math.abs(previousResult.values[1] - previousResult.values[0]), { units: ["w", "d", "h", "m", "s", "ms"], round: true });
                    }
                    return humanReadableDuration;
                }, initialText);

            }
        </script>
    </body>
</html>