<script setup lang="ts"> import * as d3 from "d3" import { onMounted, ref, watch, withDefaults, defineProps, computed } from "vue" import type { TimelineChartDataPoint } from "@/types" import { createTooltip, setEventListeners } from "@/helpers/d3/d3-tooltip" interface Props { data: TimelineChartDataPoint[], maxY?: number, width?: number } const props = defineProps<Props>() const height = 60 const marginTop = 10 const marginRight = 10 const marginBottom = 30 const marginLeft = 40 const _width = computed(() => props.width ?? 300) const _maxY = computed(() => props.maxY ?? 2) const container = ref<HTMLDivElement>() function isUp(data: TimelineChartDataPoint[], higherIsUp = true) { if (data.length === 0) return 0 const last = data[data.length - 1].value const secondLast = data.length > 1 ? data[data.length - 2].value : last const diff = higherIsUp ? last - secondLast : secondLast - last if (diff === 0) return 0 else if (diff > 0) return 1 else return -1 } watch(() => props.data, (value) => { let data = value if (data.length === 0) return // Declare the x (horizontal position) scale. const x = d3.scaleTime() .domain([data[0].date, data[data.length -1].date]) .range([marginLeft, _width.value - marginRight]) // Declare the y (vertical position) scale. const y = d3.scaleLinear() .domain([0, _maxY.value]) .range([height - marginBottom, marginTop]) // Create the SVG container. const svg = d3.create("svg") .attr("width", _width.value) .attr("height", height) // Add the x-axis. svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(d3.axisBottom(x).ticks(3).tickFormat(d3.utcFormat("%d.%m.%Y"))) // Add the y-axis. svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(d3.axisLeft(y).ticks(1)) const trend = isUp(data, false) const pathGroup = svg .append('g') .classed('path-group', true) .classed('up', trend === 1) .classed('down', trend === -1) pathGroup.append("path") .datum(data) .attr("stroke-width", 1.5) .attr("d", d3.line() .x(function(d) { return x(d.date) }) .y(function(d) { return y(d.value) }) ) const pointGroups = pathGroup.selectAll("myCircles") .data(data) .enter() .append("g") pointGroups .append('circle') .attr("r", 10) .style('fill', 'transparent') .style('cursor', 'pointer') .attr("cx", function(d) { return x(d.date) }) .attr("cy", function(d) { return y(d.value) }) .classed('chart-point', true) pointGroups .append("circle") .attr("r", 2) .attr("cx", function(d) { return x(d.date) }) .attr("cy", function(d) { return y(d.value) }) const oldTooltip = document.querySelector('.d3-tooltip') const tooltip = oldTooltip ? d3.select(oldTooltip) : createTooltip(d3.select('body'), { classes: 'border border-gray-300 bg-white p-2 rounded-md' }) setEventListeners(svg.selectAll('.chart-point'), tooltip, { useData: (d) => d.value }) // Append the SVG element. container.value.replaceChildren() container.value.append(svg.node()) }) </script> <template> <div ref="container"></div> </template> <style lang="scss"> .path-group { path { fill: none; stroke: var(--color--medium-text); } circle { fill: var(--color--medium-text); } &.up { path { stroke: var(--color--positive-text); } circle { fill: var(--color--positive-text); } } &.down { path { stroke: var(--color--negative-text); } circle { fill: var(--color--negative-text); } } } </style>