Skip to content
Snippets Groups Projects
TimelineChart.vue 3.67 KiB
Newer Older
  • Learn to ignore specific revisions
  • <script setup lang="ts">
    import * as d3 from "d3"
    
    import { ref, watch, computed } from "vue"
    
    import type { TimelineChartDataPoint } from "@/types"
    import { createTooltip, setEventListeners } from "@/helpers/d3/d3-tooltip"
    
    interface Props {
      data: TimelineChartDataPoint[],
      maxY?: number,
    
      width?: number,
      startDate: Date,
      endDate: Date
    
    }
    
    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, () => props.startDate, () => props.endDate], ([data, startDate, endDate]) => {
      if (!data || !startDate || !endDate) return
    
    
      if (data.length === 0) return
    
      // Declare the x (horizontal position) scale.
      const x = d3.scaleTime()
    
        .domain([startDate, endDate])
    
        .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.
    
      if (!container.value) return
    
      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>