diff --git a/package-lock.json b/package-lock.json index 64426464ff8c3db69ce148e0ef477990af511c05..40b79c9b2117ea4368799d953048153bcbd4d5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4181,8 +4181,8 @@ "dev": true, "optional": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "delegates": "1.0.0", + "readable-stream": "2.3.6" } }, "balanced-match": { @@ -4259,7 +4259,7 @@ "dev": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "2.3.5" } }, "fs.realpath": { @@ -4274,14 +4274,14 @@ "dev": true, "optional": true, "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.3" } }, "glob": { @@ -4290,12 +4290,12 @@ "dev": true, "optional": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } }, "has-unicode": { @@ -4310,7 +4310,7 @@ "dev": true, "optional": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": "2.1.2" } }, "ignore-walk": { @@ -4328,8 +4328,8 @@ "dev": true, "optional": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "once": "1.4.0", + "wrappy": "1.0.2" } }, "inherits": { @@ -4374,10 +4374,9 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "safe-buffer": "5.1.2", + "yallist": "3.0.3" } }, "minizlib": { @@ -4386,7 +4385,7 @@ "dev": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "2.3.5" } }, "mkdirp": { @@ -4409,9 +4408,9 @@ "dev": true, "optional": true, "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" + "debug": "2.6.9", + "iconv-lite": "0.4.24", + "sax": "1.2.4" } }, "node-pre-gyp": { @@ -4438,8 +4437,8 @@ "dev": true, "optional": true, "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1.1.1", + "osenv": "0.1.5" } }, "npm-bundled": { @@ -4464,10 +4463,10 @@ "dev": true, "optional": true, "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "1.1.5", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" } }, "number-is-nan": { @@ -4485,9 +4484,8 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { - "wrappy": "1" + "wrappy": "1.0.2" } }, "os-homedir": { @@ -4508,8 +4506,8 @@ "dev": true, "optional": true, "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" } }, "path-is-absolute": { @@ -4530,10 +4528,10 @@ "dev": true, "optional": true, "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "deep-extend": "0.6.0", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" }, "dependencies": { "minimist": { @@ -4550,13 +4548,13 @@ "dev": true, "optional": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" } }, "rimraf": { @@ -4565,14 +4563,13 @@ "dev": true, "optional": true, "requires": { - "glob": "^7.1.3" + "glob": "7.1.3" } }, "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4608,11 +4605,10 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, "string_decoder": { @@ -4621,16 +4617,15 @@ "dev": true, "optional": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "5.1.2" } }, "strip-ansi": { "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "2.1.1" } }, "strip-json-comments": { @@ -4666,20 +4661,18 @@ "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "1.0.2" } }, "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/package.json b/package.json index c3aebd5ea1cab5070a40af3fb867fba5963ca77b..154057e9ccc9cacc91627a8bbf671b6fb7867fc7 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bootstrap": "^4.3.1", "bootstrap-vue": "^2.0.0-rc.15", "d3": "^5.9.2", + "d3-scale-chromatic": "^1.3.3", "vue": "^2.6.10", "vue-cookie": "^1.1.4", "vue-router": "^3.0.1", diff --git a/src/components/pages/BiayaLangsungPage.vue b/src/components/pages/BiayaLangsungPage.vue index a5b5ec0e2f6a088933b462dea049dd1ccfa76804..2150deb80ce3cbb600fbd1106a873514e645e49f 100644 --- a/src/components/pages/BiayaLangsungPage.vue +++ b/src/components/pages/BiayaLangsungPage.vue @@ -3,14 +3,14 @@ <b-breadcrumb class="bg-light" :items="items"></b-breadcrumb> <h2>ANGGARAN BIAYA LANGSUNG APBD JABAR</h2> <b-row class="justify-content-md-center"> - <b-col id="visualization" class="bg-light" cols="12" lg="8"> + <b-col id="visualization" class="bg-light" cols="12"> <visualization v-bind:src="data" @click-handler="visualizationHandler" ref="viz" /> </b-col> - <b-col cols="12" lg="4"> + <b-col cols="12"> <detail-card :detailAnggaran='items' /> </b-col> </b-row> diff --git a/src/components/partials/DetailCard.vue b/src/components/partials/DetailCard.vue index 49aeee219c067226ec130a7eb4b7a725e5ef83d0..a2ed4b28d0370d19d599f7962e006b95e15e8260 100644 --- a/src/components/partials/DetailCard.vue +++ b/src/components/partials/DetailCard.vue @@ -42,7 +42,7 @@ export default { watch: { detailAnggaran(val) { // Exclude top level when the top level is 'Semua Anggaran' - if (val[0].name = 'Semua Anggaran') val = val.slice(1, val.length) + if (val.length > 0 && val[0].text == 'Semua Anggaran') val = val.slice(1, val.length) this.head = val[0] this.tail = val.slice(1, val.length) diff --git a/src/components/partials/Visualization.vue b/src/components/partials/Visualization.vue index 5287f6c30a0b7f7d48e7e6d99f331805d116f32e..871f4131eff51c90c60bb5c66176013dc55454ef 100644 --- a/src/components/partials/Visualization.vue +++ b/src/components/partials/Visualization.vue @@ -1,12 +1,25 @@ <template> <div id="placeholder"> - <div id="chart-container"> - <h3 - v-if="!hide" - class="align-middle text-muted" - > - Memuat data - </h3> + <div id="chart-container" ref="container"> + <div id="options-container"> + <h3 + v-if="!hide" + class="text-center text-muted" + > + Memuat data + </h3> + <div id="options" v-else> + <b class="mr-3">Tata letak</b> + <b-form-group class="mb-0"> + <b-form-radio-group v-model="selected" name="position-options"> + <b-form-radio value="normal">Normal</b-form-radio> + <b-form-radio value="sorted">Terurut</b-form-radio> + <b-form-radio value="separated">Pisah</b-form-radio> + <b-form-input v-if="selected == 'separated'" v-model="separator" type="range" :min="minRadius" :max="maxRadius"></b-form-input> + </b-form-radio-group> + </b-form-group> + </div> + </div> <svg id="chart"/> </div> </div> @@ -25,24 +38,85 @@ export default { return { hide: false, svg: null, - tooltip: null + tooltip: null, + simulation: null, + selected: 'normal', + radiusCircles: null, + maxRadius: 60, + minRadius: 3, + separator: 20, + width: 600, + height: 400, + columnForColor: "name", + columnForWidth: "value", + columnId: "_id", + columnPercentage: "percentage", + columnSubdata: "subdata", + ticked: null, } }, + mounted() { + window.addEventListener("resize", this.sizeHandler) + this.width = this.$refs.container.clientWidth + this.height = this.$refs.container.clientHeight + }, + destroyed() { + window.removeEventListener("resize", this.sizeHandler) + }, watch: { src(val) { - d3.select("#chart-container").datum(val).call(this.bubbleChart()); - this.hide = true + this.createChart() + }, + width(val) { + this.clear() + this.createChart() + }, + height(val) { + this.clear() + this.createChart() + }, + selected(val) { + this.setSimulation() + }, + separator(val) { + this.setSimulation() } }, methods: { clear() { - this.svg.selectAll("*").remove() - this.tooltip.remove() + if (this.svg) this.svg.selectAll("*").remove() + if (this.tooltip) this.tooltip.remove() this.hide = false }, + createChart() { + d3.select("#chart-container").datum(this.src).call(this.bubbleChart()); + this.hide = true + }, + sizeHandler() { + this.height = this.$refs.container.clientHeight + this.width = this.$refs.container.clientWidth + }, + setSimulation() { + let self = this + this.simulation = d3.forceSimulation(this.src) + .force('charge', d3.forceManyBody().strength(-20)) + .force('collision', d3.forceCollide().radius(function(d) { + return self.radiusCircles(d[self.columnForWidth]) + })) + .force('x', d3.forceX(function(d) { + if (self.selected == 'sorted') + return self.width*(self.radiusCircles(d[self.columnForWidth])/self.maxRadius - 0.5)*0.75 + else if (self.selected == 'normal') + return 0 + else if (self.selected == 'separated') + return self.radiusCircles(d[self.columnForWidth]) < self.separator ? -self.width/3 : self.width/3 + })) + .force('y', d3.forceY()) + .on('tick', this.ticked) + }, bubbleChart() { - var width = 600, - height = 400, + var width = this.width, + height = this.height, maxRadius = 60, minRadius = 3, columnForColor = "name", @@ -58,7 +132,7 @@ export default { var data = selection.datum() var div = selection self.svg = div.select("svg") - self.svg.attr("width", width).attr("height", height) + self.svg.attr("width", self.width).attr("height", self.height) //put the data (hidden) to be visible when mouseover self.tooltip = selection @@ -78,7 +152,7 @@ export default { // ticked handler function - function ticked(e) { + self.ticked = function(e) { node.attr("cx", function(d) { return d.x }) @@ -88,24 +162,17 @@ export default { } //create circles width by data - var radiusCircles = d3.scaleSqrt().domain([d3.min(data, function(d) { - return +d[columnForWidth] + self.radiusCircles = d3.scaleSqrt().domain([d3.min(data, function(d) { + return +d[self.columnForWidth] }), d3.max(data, function(d) { - return +d[columnForWidth] - })]).range([minRadius,maxRadius]) // map the min-max data in the column for width to 5-maxRadius + return +d[self.columnForWidth] + })]).range([self.minRadius,self.maxRadius]) //create circle colors by data - var colorCircles = d3.scaleOrdinal(d3.schemeCategory10) + var colorCircles = d3.scaleOrdinal(d3.schemeBrBG[11]) // make force simulation function - var simulation = d3.forceSimulation(data) - .force('charge', d3.forceManyBody().strength(-20)) - .force('collision', d3.forceCollide().radius(function(d) { - return radiusCircles(d[columnForWidth]) - })) - .force('x', d3.forceX()) - .force('y', d3.forceY()) - .on("tick", ticked) + self.setSimulation() //create the circles var node = self.svg.selectAll("circle") @@ -113,10 +180,10 @@ export default { .enter() .append("circle") .attr('r', function(d) { - return radiusCircles(d[columnForWidth]) + return self.radiusCircles(d[self.columnForWidth]) }) .style("fill", function(d) { - return colorCircles(d[columnForColor]) + return d3.interpolateViridis(self.radiusCircles(d[self.columnForWidth])/self.maxRadius) }) .attr('transform', 'translate(' + [width / 2, height / 2] + ')') @@ -124,21 +191,21 @@ export default { .on("mouseover", function(d) { self.tooltip.html( "<h5 style='margin-bottom: 0;'>" + - d[columnForColor] + + d[self.columnForColor] + "</h5><b>" + // "<h6 style='margin-bottom: 0;'>" + // d[columnPercentage].toFixed(2) + // "%</h6><b>" + "Rp " + - d[columnForWidth].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".") + + d[self.columnForWidth].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".") + ",00.-</b>" ); return self.tooltip.style("visibility", "visible") }) .on("mouseup", function(d) { - if (d[columnSubdata].length != 0) { + if (d[self.columnSubdata].length != 0) { self.clear() - self.$emit('click-handler', d[columnId]) + self.$emit('click-handler', d[self.columnId]) } }) .on("mousemove", function() { @@ -151,43 +218,43 @@ export default { chart.width = function(val){ if (!arguments) { - return width + return self.width } else { - width = val + self.width = val } return chart } chart.height = function(val) { if (!arguments) { - return height + return self.height } else { - height = val + self.height = val } return chart } chart.columnForColor = function(value) { if (!arguments.columnForColor) { - return columnForColor + return self.columnForColor } - columnForColor = value + self.columnForColor = value return chart }; chart.columnForWidth = function(value) { if (!arguments.columnForWidth) { - return columnForWidth + return self.columnForWidth } - columnForWidth = value + self.columnForWidth = value return chart }; chart.columnId = function(value) { if (!arguments.columnId) { - return columnId + return self.columnId } - columnId = value + self.columnId = value return chart }; @@ -204,7 +271,6 @@ export default { #placeholder { width: 100%; height: 100%; - padding: 20px; #chart-container { display: flex; @@ -215,6 +281,16 @@ export default { margin: auto; } } + + #options-container { + position: absolute; + padding: 1rem; + background-color: #ffffff44; + + #options { + display: flex; + } + } } </style>