view client/src/components/fairway/AvailableFairwayDepth.vue @ 4805:7de099c4824c

client: image-export: improve hyperlink ids for download * use own id-names for <a> element for each diagram to avoid having same id name in dom in case of two diagrams on screen
author Fadi Abbud <fadi.abbud@intevation.de>
date Mon, 28 Oct 2019 12:24:35 +0100
parents b3f65cff13e8
children db450fcc8ed7
line wrap: on
line source

<template>
  <div class="d-flex flex-column flex-fill">
    <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
    <UISpinnerOverlay v-if="loading" />
    <div class="d-flex flex-fill">
      <DiagramLegend>
        <div v-for="(entry, index) in legend" :key="index" class="legend">
          <span
            :style="
              `${legendStyle(
                index
              )}; border-radius: 0.25rem; width: 40px; height: 20px;`
            "
          ></span>
          {{ entry }}
        </div>
        <div>
          <select
            @change="applyChange"
            v-model="form.template"
            class="form-control d-block custom-select-sm w-100 mt-1"
          >
            <option
              v-for="template in templates"
              :value="template"
              :key="template.name"
            >
              {{ template.name }}
            </option>
          </select>
          <button
            @click="downloadPDF"
            type="button"
            class="btn btn-sm btn-info d-block w-100 mt-2"
          >
            <translate>Export to PDF</translate>
          </button>
          <a
            :href="dataLink"
            :download="`${fileName}.csv`"
            class="mt-2 btn btn-sm btn-info w-100"
            ><translate>Download CSV</translate></a
          >
          <a
            @click="downloadImage('AFDpng')"
            id="AFDpng"
            class="btn btn-sm btn-info text-white d-block w-100 mt-2"
            :download="`${fileName}.png`"
          >
            <translate>Download Image</translate>
          </a>
        </div>
        <div class="btn-group-toggle w-100 mt-2">
          <label
            class="btn btn-outline-secondary btn-sm"
            :class="{ active: showNumbers }"
            ><input
              type="checkbox"
              v-model="showNumbers"
              autocomplete="off"
            /><translate>Numbers</translate>
          </label>
        </div>
      </DiagramLegend>
      <div
        ref="diagramContainer"
        :id="containerId"
        class="diagram-container flex-fill"
      ></div>
    </div>
  </div>
</template>

<style></style>

<script>
/* This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018, 2019 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * * Thomas Junk <thomas.junk@intevation.de>
 * * Markus Kottländer <markus.kottlaender@intevation.de>
 * * Fadi Abbud <fadi.abbud@intevation.de>
 * * Bernhard Reiter <bernhard.reiter@intevation.de>
 */
import * as d3 from "d3";
import app from "@/main";
import debounce from "debounce";
import { mapState } from "vuex";
import filters from "@/lib/filters.js";
import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
import { HTTP } from "@/lib/http";
import { displayError } from "@/lib/errors";
import { FREQUENCIES } from "@/store/fairwayavailability";
import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
import { localeDateString } from "@/lib/datelocalization";

export default {
  mixins: [diagram, pdfgen, templateLoader],
  components: {
    DiagramLegend: () => import("@/components/DiagramLegend")
  },
  data() {
    return {
      frequencyD: null,
      selectedFairwayAvailabilityFeatureD: null,
      fromDate: null,
      toDate: null,
      depthlimit1D: null,
      depthlimit2D: null,
      widthlimit1D: null,
      widthlimit2D: null,
      containerId: "availablefairwaydepth-diagram-container",
      resizeListenerFunction: null,
      loading: false,
      scalePaddingLeft: 60,
      scalePaddingRight: 0,
      paddingTop: 25,
      pdf: {
        doc: null,
        width: null,
        height: null
      },
      form: {
        template: null
      },
      templateData: null,
      templates: [],
      defaultTemplate: defaultDiagramTemplate,
      showNumbers: false
    };
  },
  created() {
    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
    window.addEventListener("resize", this.resizeListenerFunction);
  },
  destroyed() {
    window.removeEventListener("resize", this.resizeListenerFunction);
  },
  mounted() {
    // Nasty but necessary if we don't want to use the updated hook to re-draw
    // the diagram because this would re-draw it also for irrelevant reasons.
    // In this case we need to wait for the child component (DiagramLegend) to
    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
    // should be possible with $nextTick() but it doesn't work because it does
    // not guarantee that the DOM is not only updated but also re-painted on the
    // screen.
    setTimeout(this.drawDiagram, 150);
    this.initDiagramValues();
    this.templates[0] = this.defaultTemplate;
    this.form.template = this.templates[0];
    this.templateData = this.form.template;
    HTTP.get("/templates/diagram", {
      headers: {
        "X-Gemma-Auth": localStorage.getItem("token"),
        "Content-type": "text/xml; charset=UTF-8"
      }
    })
      .then(response => {
        if (response.data.length) {
          this.templates = response.data;
          this.form.template = this.templates[0];
          this.templates[this.templates.length] = this.defaultTemplate;
          this.applyChange();
        }
      })
      .catch(e => {
        const { status, data } = e.response;
        displayError({
          title: this.$gettext("Backend Error"),
          message: `${status}: ${data.message || data}`
        });
      });
  },
  computed: {
    ...mapState("fairwayavailability", [
      "selectedFairwayAvailabilityFeature",
      "fwData",
      "from",
      "to",
      "frequency",
      "csv",
      "depthlimit1",
      "depthlimit2",
      "widthlimit1",
      "widthlimit2"
    ]),
    legend() {
      const d = [this.depthlimit1D, this.depthlimit2D].sort();
      const w = [this.widthlimit1D, this.widthlimit2D].sort();
      const lowerBound = [d[0] / 100, w[0]].filter(x => x).join(", ");
      const upperBound = [d[1] / 100, w[1]].filter(x => x).join(", ");
      let result;
      if (this.depthlimit1D !== this.depthlimit2D) {
        result = [
          `> LDC`,
          `>= ${upperBound} [m]`,
          `< ${upperBound} [m]`,
          `< ${lowerBound} [m]`
        ];
      } else {
        result = [`> LDC`, `>= ${upperBound} [m]`, `< ${upperBound} [m]`];
      }
      return result;
    },
    dataLink() {
      return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`;
    },
    fileName() {
      if (!this.frequencyD) return;
      return this.downloadFilename(
        this.$gettext("FairwayAvailability"),
        this.featureName
      );
    },
    csvFileName() {
      if (!this.frequencyD) return;
      return (
        this.downloadFilename(
          this.$gettext("FairwayAvailability"),
          this.featureName
        ) + ".csv"
      );
    },
    frequencyToRange() {
      if (!this.frequencyD) return;
      const frequencies = {
        [FREQUENCIES.MONTHLY]: [-33, 33],
        [FREQUENCIES.QUARTERLY]: [-93, 93],
        [FREQUENCIES.YEARLY]: [-370, 370]
      };
      return frequencies[this.frequencyD];
    },
    availability() {
      return this.plainAvailability;
    },
    title() {
      if (!this.frequencyD) return;
      return `${this.$gettext("Available Fairway Depth:")} ${
        this.featureName
      } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate(
        this.toDate
      )}) ${this.$gettext(this.frequencyD)}`;
    },
    featureName() {
      if (this.selectedFairwayAvailabilityFeatureD == null) return "";
      return this.selectedFairwayAvailabilityFeatureD.properties.name;
    }
  },
  methods: {
    initDiagramValues() {
      this.selectedFairwayAvailabilityFeatureD = this.selectedFairwayAvailabilityFeature;
      this.fromDate = this.from;
      this.toDate = this.to;
      this.depthlimit1D = this.depthlimit1;
      this.depthlimit2D = this.depthlimit2;
      this.widthlimit1D = this.widthlimit1;
      this.widthlimit2D = this.widthlimit2;
      this.frequencyD = this.frequency;
    },
    applyChange() {
      if (this.form.template.hasOwnProperty("properties")) {
        this.templateData = this.defaultTemplate;
        return;
      }
      if (this.form.template) {
        this.loadTemplates("/templates/diagram/" + this.form.template.name)
          .then(response => {
            this.prepareImages(response.data.template_data.elements).then(
              values => {
                values.forEach(v => {
                  response.data.template_data.elements[v.index].url = v.url;
                });
                this.templateData = response.data.template_data;
              }
            );
          })
          .catch(e => {
            const { status, data } = e.response;
            displayError({
              title: this.$gettext("Backend Error"),
              message: `${status}: ${data.message || data}`
            });
          });
      }
    },
    downloadPDF() {
      let title = `${this.$gettext("Available Fairway Depth:")} ${
        this.featureName
      }`;
      this.generatePDF({
        templateData: this.templateData,
        diagramTitle: title
      });
      this.pdf.doc.save(this.fileName + ".pdf");
    },
    addDiagramLegend(position, offset, color) {
      let x = offset.x,
        y = offset.y;
      this.pdf.doc.setFontSize(10);
      let width =
        (this.pdf.doc.getStringUnitWidth(">= LDC") * 10) / (72 / 25.6) + 15;
      // if position is on the right, x needs to be calculate with pdf width and
      // the size of the element
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x = this.pdf.width - offset.x - width;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y = this.pdf.height - offset.y - this.getTextHeight(6);
      }

      if (this.legend[3]) {
        this.pdf.doc.setTextColor(color);
        this.pdf.doc.setDrawColor(this.$options.COLORS.LDC);
        this.pdf.doc.setFillColor(this.$options.COLORS.LDC);
        this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[0], x + 12, y + 3);

        this.pdf.doc.setDrawColor(this.$options.COLORS.HIGHEST);
        this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST);
        this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[1], x + 12, y + 8);

        this.pdf.doc.setDrawColor(this.$options.COLORS.REST[1]);
        this.pdf.doc.setFillColor(this.$options.COLORS.REST[1]);
        this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[2], x + 12, y + 13);

        this.pdf.doc.setDrawColor(this.$options.COLORS.REST[0]);
        this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]);
        this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[3], x + 12, y + 18);
      } else {
        this.pdf.doc.setTextColor(color);
        this.pdf.doc.setDrawColor(this.$options.COLORS.LDC);
        this.pdf.doc.setFillColor(this.$options.COLORS.LDC);
        this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[0], x + 12, y + 3);

        this.pdf.doc.setDrawColor(this.$options.COLORS.HIGHEST);
        this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST);
        this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[1], x + 12, y + 8);

        this.pdf.doc.setDrawColor(this.$options.COLORS.REST[0]);
        this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]);
        this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD");
        this.pdf.doc.text(this.legend[2], x + 12, y + 13);
      }
    },
    legendStyle(index) {
      if (this.depthlimit1 === this.depthlimit2) {
        let result = [
          `background-color: ${this.$options.COLORS.LDC};`,
          `background-color: ${this.$options.COLORS.HIGHEST};`
        ];
        this.fwData[0].lowerLevels.forEach((e, i) => {
          result.push(`background-color: ${this.$options.COLORS.REST[i]};`);
        });
        return result[index];
      }
      return [
        `background-color: ${this.$options.COLORS.LDC};`,
        `background-color: ${this.$options.COLORS.HIGHEST};`,
        `background-color: ${this.$options.COLORS.REST[1]};`,
        `background-color: ${this.$options.COLORS.REST[0]};`
      ][index];
    },
    close() {
      this.$store.commit("application/paneSetup", "DEFAULT");
    },
    getPrintLayout(svgHeight, svgWidth) {
      return {
        main: {
          top: 0,
          right: Math.floor(svgWidth * 0.025),
          bottom: Math.floor(svgHeight * 0.17),
          left: Math.floor(svgWidth * 0.025)
        }
      };
    },
    drawDiagram() {
      d3.timeFormatDefaultLocale(localeDateString);
      const elem = document.querySelector("#" + this.containerId);
      const svgWidth = elem != null ? elem.clientWidth : 0;
      const svgHeight = elem != null ? elem.clientHeight : 0;
      const layout = this.getPrintLayout(svgHeight, svgWidth);
      const dimensions = this.getDimensions({
        svgHeight,
        svgWidth,
        ...layout
      });
      d3.select(".diagram-container svg").remove();
      this.renderTo({ element: ".diagram-container", dimensions });
    },
    renderTo({ element, dimensions }) {
      const diagram = d3
        .select(element)
        .append("svg")
        .attr("width", "100%")
        .attr("height", "100%");
      diagram.append("g");
      diagram
        .append("g")
        .append("rect")
        .attr("width", "100%")
        .attr("height", "100%")
        .attr("fill", "#ffffff");
      const yScale = d3
        .scaleLinear()
        .domain(this.frequencyToRange)
        .range([dimensions.mainHeight - 30, 0]);
      this.drawScaleLabel({ diagram, dimensions });
      this.drawScale({ diagram, dimensions, yScale });
      this.drawBars({ diagram, yScale, dimensions });
      this.drawTooltip(diagram);
    },
    drawTooltip(diagram) {
      diagram
        .append("text")
        .text("")
        .attr("font-size", "0.8em")
        .attr("opacity", 0)
        .attr("id", "tooltip");
    },
    drawBars({ diagram, yScale, dimensions }) {
      const widthPerItem = Math.min(
        (dimensions.width - this.scalePaddingLeft - this.scalePaddingRight) /
          this.fwData.length,
        180
      );
      const spaceBetween = widthPerItem * 0.2;
      const ldcOffset = widthPerItem * 0.1;
      const everyBar = diagram
        .selectAll("g.bars")
        .data(this.fwData)
        .enter()
        .append("g")
        .attr("class", "bars")
        .attr("transform", (d, i) => {
          const dx = this.scalePaddingLeft + i * widthPerItem;
          return `translate(${dx})`;
        });
      this.drawSingleBars({
        everyBar,
        yScale,
        dimensions,
        widthPerItem,
        spaceBetween,
        ldcOffset
      });
      this.drawLabelPerBar({ everyBar, dimensions, widthPerItem });
    },
    drawSingleBars({
      everyBar,
      yScale,
      widthPerItem,
      spaceBetween,
      ldcOffset
    }) {
      this.drawLDC({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset });
      this.drawHighestLevel({
        everyBar,
        yScale,
        widthPerItem,
        spaceBetween,
        ldcOffset
      });
      this.drawLowerLevels({
        everyBar,
        yScale,
        widthPerItem,
        spaceBetween,
        ldcOffset
      });
    },
    drawLowerLevels({
      everyBar,
      yScale,
      widthPerItem,
      spaceBetween,
      ldcOffset
    }) {
      everyBar
        .selectAll("g.bars")
        .data(d => d.lowerLevels)
        .enter()
        .append("rect")
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.8");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(d.height)
            .attr("y", y - 10)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("y", d => 2 * yScale(0) - yScale(d.translateY) + this.paddingTop)
        .attr("height", d => {
          return yScale(0) - yScale(d.height);
        })
        .attr("x", ldcOffset + spaceBetween / 2)
        .attr("width", widthPerItem - ldcOffset - spaceBetween)
        .attr("id", "lower")
        .attr("fill", (d, i) => {
          return this.$options.COLORS.REST[i];
        });
      if (this.showNumbers) {
        everyBar
          .selectAll("g.bars")
          .data(d => d.lowerLevels)
          .enter()
          .filter(d => d.height > 0)
          .insert("text")
          .attr("y", d => {
            return (
              2 * yScale(0) -
              yScale(d.translateY) +
              this.paddingTop +
              (yScale(0) - yScale(d.height)) +
              (yScale(0) - yScale(1.9)) //instead o alignment-baseline hanging
            );
          })
          .attr("x", widthPerItem / 2)
          .text(d => d.height)
          // does not work with svg2pdf .attr("alignment-baseline", "hanging")
          .attr("text-anchor", "middle")
          .attr("font-size", "8")
          .attr("fill", "black");
      }
    },
    fnheight({ name, yScale }) {
      return d => yScale(0) - yScale(d[name]);
    },
    drawLDC({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) {
      const height = this.fnheight({ name: "ldc", yScale });
      everyBar
        .append("rect")
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.7");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(d.ldc)
            .attr("y", y - 50)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("y", yScale(0))
        .attr("height", height)
        .attr("x", spaceBetween / 2)
        .attr("width", widthPerItem - ldcOffset - spaceBetween)
        .attr(
          "transform",
          d => `translate(0 ${this.paddingTop + -1 * height(d)})`
        )
        .attr("fill", this.$options.COLORS.LDC)
        .attr("id", "ldc");
      if (this.showNumbers) {
        everyBar
          .filter(d => d.ldc > 0)
          .append("text")
          .attr("y", yScale(0.5)) // some distance from the bar
          .attr("x", spaceBetween / 2)
          .text(d => d.ldc)
          .attr("text-anchor", "left")
          .attr("font-size", "8")
          .attr(
            "transform",
            d => `translate(0 ${this.paddingTop + -1 * height(d)})`
          )
          .attr("fill", "black");
      }
    },
    drawHighestLevel({
      everyBar,
      yScale,
      widthPerItem,
      spaceBetween,
      ldcOffset
    }) {
      const height = this.fnheight({ name: "highestLevel", yScale });
      everyBar
        .append("rect")
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.8");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(d.highestLevel)
            .attr("y", y - 50)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("y", yScale(0))
        .attr("height", height)
        .attr("x", ldcOffset + spaceBetween / 2)
        .attr("width", widthPerItem - ldcOffset - spaceBetween)
        .attr(
          "transform",
          d => `translate(0 ${this.paddingTop + -1 * height(d)})`
        )
        .attr("fill", this.$options.COLORS.HIGHEST);
      if (this.showNumbers) {
        everyBar
          .filter(d => d.highestLevel > 0)
          .append("text")
          .attr("y", yScale(0.5)) // some distance from the bar
          .attr("x", widthPerItem / 2)
          .text(d => d.highestLevel)
          .attr("text-anchor", "middle")
          .attr("font-size", "8")
          .attr(
            "transform",
            d => `translate(0 ${this.paddingTop + -1 * height(d)})`
          )
          .attr("fill", "black");
      }
    },
    drawLabelPerBar({ everyBar, dimensions, widthPerItem }) {
      everyBar
        .append("text")
        .text(d => d.label)
        .attr("y", dimensions.mainHeight + this.paddingTop - 5)
        .attr("x", widthPerItem / 2)
        .attr("text-anchor", "middle")
        .attr("font-size", 10);
    },
    drawScaleLabel({ diagram, dimensions }) {
      diagram
        .append("text")
        .text(this.$options.LEGEND)
        .attr("text-anchor", "middle")
        .attr("x", 0)
        .attr("y", 0)
        .attr("dy", "20")
        .attr(
          "transform",
          `translate(2, ${(dimensions.mainHeight + this.paddingTop) /
            2}), rotate(-90)`
        );
    },
    drawScale({ diagram, dimensions, yScale }) {
      const yAxisLeft = d3
        .axisLeft()
        .tickSizeInner(
          dimensions.width - this.scalePaddingLeft - this.scalePaddingRight
        )
        .tickSizeOuter(0)
        .scale(yScale);
      const yAxisRight = d3
        .axisRight()
        .tickSizeInner(
          dimensions.width - this.scalePaddingLeft - this.scalePaddingRight
        )
        .tickSizeOuter(0)
        .scale(yScale);

      diagram
        .append("g")
        .attr(
          "transform",
          `translate(${dimensions.width - this.scalePaddingRight} ${
            this.paddingTop
          })`
        )
        .call(yAxisLeft)
        .selectAll(".tick text")
        .attr("fill", "black")
        .attr("font-size", 10)
        .attr("dy", 3)
        .attr("dx", -3)
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke-dasharray", 5)
        .attr("stroke", "#ccc")
        .select(function() {
          return this.parentNode;
        })
        .filter(d => d === 0)
        .selectAll(".tick line")
        .attr("stroke-dasharray", "none")
        .attr("stroke", "#333");
      diagram
        .append("g")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft} ${this.paddingTop})`
        )
        .call(yAxisRight)
        .selectAll(".tick text")
        .attr("fill", "black")
        .attr("font-size", 10)
        .attr("dy", 3)
        .attr("dx", 3)
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "transparent");
      diagram.selectAll(".domain").attr("stroke", "black");
    }
  },
  watch: {
    fwData() {
      this.initDiagramValues();
      this.drawDiagram();
    },
    showNumbers() {
      this.drawDiagram();
    }
  },
  LEGEND: app.$gettext("Sum of days"),
  COLORS: {
    LDC: "aqua",
    HIGHEST: "blue",
    REST: ["hotpink", "darksalmon", "#ffaaaa"]
  }
};
</script>